Skip to content

STM32 Platform Guide

Xylolabs SDK integration for STM32 microcontrollers.


Supported Targets

MCU Core Clock Flash RAM Audio Capability
STM32F103C8 (Blue Pill) Cortex-M3 72 MHz 64 KB 20 KB Sensors only (ADPCM 1–2ch optional)
STM32F411CE (Black Pill) Cortex-M4F + FPU 100 MHz 512 KB 128 KB 4ch XAP @48kHz
STM32WB55RG (Nucleo WB) Cortex-M4F + FPU 64 MHz 1 MB 256 KB 4ch XAP @48kHz (BLE 5.3, internal ADC)
STM32WBA55CG (DK board) Cortex-M33 + FPU 100 MHz 1 MB 128 KB 4ch XAP @96kHz (BLE 5.4, internal ADC)

Hardware Setup

I2S ADC Connection (F411 / WB55)

The SDK expects interleaved PCM samples from a stereo I2S ADC (e.g. two ICS-43434 or INMP441 microphones providing left/right channels).

MCU I2S peripheral        ADC (e.g. ICS-43434)
─────────────────────     ──────────────────────
I2S_CK  (PB13)     ───── BCK
I2S_WS  (PB12)     ───── WS / LRCK
I2S_SD  (PB15)     ───── SD (data in)
3V3                ───── VDD
GND                ───── GND
PB14 (GPIO)        ───── L/R select (GND=left, 3V3=right)

For 4-channel audio, connect two I2S buses (or one bus with L/R select tied differently and merge in firmware).

LTE Modem (UART) — BG770A

MCU UART2              LTE Modem
──────────────────     ─────────────────
PA2  (TX)       ─────  RXD
PA3  (RX)       ─────  TXD
PA0  (GPIO)     ─────  PWRKEY
PA1  (GPIO)     ─────  RESET_N
3V3 / VBAT      ─────  VCC (check modem datasheet for voltage)
GND             ─────  GND

Sensor Buses

Bus Pins (F411 example) Typical devices
I2C1 PB6 (SCL), PB7 (SDA) BME280 (temp/humidity), LIS3DH (vibration)
SPI1 PA5/PA6/PA7 + CS MAX31865 (RTD), ADS1256 (ADC)
ADC1 PA0–PA7 Current sensors, voltage dividers

# Install dependencies
rustup target add thumbv7em-none-eabihf     # For F411/WB55
rustup target add thumbv7m-none-eabi        # For F103
rustup target add thumbv8m.main-none-eabihf # For WBA55
cargo install probe-rs-tools

# Build
cd sdk/examples/stm32f411-audio
cargo build --release

# Flash via ST-Link
cargo run --release

See sdk/examples/ for all STM32 examples.


Legacy C Build System

Option A: STM32CubeIDE

  1. Create a new STM32 project for your target (F411CE or F103C8).
  2. Enable I2S, UART2, I2C1, SPI1 peripherals in CubeMX.
  3. Add the SDK as a linked folder:
  4. Right-click project → Properties → C/C++ General → Paths and Symbols
  5. Add sdk/c/common/include to include paths
  6. Add sdk/c/common/src and sdk/c/stm32/src source folders
  7. Add compile definitions in project properties:
    XYLOLABS_PLATFORM_STM32
    
    For F103 (ADPCM legacy path):
    XYLOLABS_ENABLE_ADPCM=1
    XYLOLABS_AUDIO_CODEC=XYLOLABS_CODEC_ADPCM
    

Option B: CMake + arm-none-eabi-gcc

cmake_minimum_required(VERSION 3.22)
project(my_device C)

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_C_FLAGS "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -Os")

add_subdirectory(sdk/c/stm32)

add_executable(my_device
    src/main.c
    src/platform_stm32.c
)

target_link_libraries(my_device PRIVATE xylolabs_stm32)
target_include_directories(my_device PRIVATE sdk/c/common/include)

For F103 (Cortex-M3, no FPU):

set(CMAKE_C_FLAGS "-mcpu=cortex-m3 -mthumb -Os")
target_compile_definitions(my_device PRIVATE
    XYLOLABS_ENABLE_ADPCM=1
    XYLOLABS_AUDIO_CODEC=XYLOLABS_CODEC_ADPCM
    XYLOLABS_AUDIO_CHANNELS=0   # sensor-only
)


Memory Budget

STM32F103 (20 KB RAM) — Sensor-Only

Region Size
XMBP packet buffer 2 KB
HTTP buffer 1 KB
Metadata ring 2 KB
Stack + HAL ~8 KB
Total SDK ~5 KB
Available for app ~7 KB

Recommended overrides:

// Rust (Embassy) - Recommended
// Memory-constrained config for F103 (20 KB RAM) — set in build.rs
const AUDIO_CHANNELS: usize = 0;
const XMBP_BUF_SIZE: usize = 2048;
const HTTP_BUF_SIZE: usize = 1024;
const META_RING_SIZE: usize = 2048;
const SENSOR_CHANNELS: usize = 2;
const MOTOR_CHANNELS: usize = 0;

Legacy C equivalent
#define XYLOLABS_AUDIO_CHANNELS    0
#define XYLOLABS_XMBP_BUF_SIZE    2048
#define XYLOLABS_HTTP_BUF_SIZE    1024
#define XYLOLABS_META_RING_SIZE   2048
#define XYLOLABS_SENSOR_CHANNELS  2
#define XYLOLABS_MOTOR_CHANNELS   0

STM32F411 (128 KB RAM) — Full Audio

Region Size
Audio ring buffer 32 KB
XAP batch buffer ~4 KB
XMBP packet buffer 16 KB
HTTP buffer 4 KB
Metadata ring 8 KB
XAP encoder state (4ch) ~4 KB
Stack + HAL ~12 KB
Total SDK ~68 KB
Available for app ~60 KB

XAP Codec Feasibility

XAP and XMBP are patent-pending technologies of Xylolabs Inc.

Target Cores Clock XAP Budget Feasibility
STM32F103 1x M3 72 MHz ~72 MIPS Insufficient for 4ch @48kHz (~40 MIPS needed); use ADPCM or sensors-only
STM32F411 1x M4F 100 MHz ~100 MIPS Sufficient for 4ch @48kHz (~40 MIPS); leaves ~60 MIPS for app
STM32WB55 1x M4F 64 MHz ~64 MIPS Sufficient for 4ch @48kHz (~39 MIPS with DSP); M0+ core handles BLE
STM32WBA55 1x M33 100 MHz ~100 MIPS Sufficient for 4ch @96kHz (~56 MIPS with DSP); integrated radio shares CPU

F103 ADPCM fallback: For STM32F103 deployments that require audio (1–2 channels at reduced rate), enable the ADPCM legacy path. ADPCM requires <1 MIPS/channel, making it viable on the M3.

// Rust (Embassy) - Recommended
// F103 ADPCM fallback config — set in build.rs or Cargo.toml features
const ENABLE_ADPCM: bool = true;
const AUDIO_CODEC: Codec = Codec::Adpcm;
const AUDIO_CHANNELS: usize = 2;     // 1–2ch only on F103
const AUDIO_SAMPLE_RATE: u32 = 8000;
Legacy C equivalent
// In your project config or CMakeLists.txt:
#define XYLOLABS_ENABLE_ADPCM        1
#define XYLOLABS_AUDIO_CODEC         XYLOLABS_CODEC_ADPCM
#define XYLOLABS_AUDIO_CHANNELS      2   // 1–2ch only on F103
#define XYLOLABS_AUDIO_SAMPLE_RATE   8000

Rust is the recommended language for new STM32 projects where Embassy support is mature (F411, WB55, U5 series). The Embassy-based Rust examples provide async tasks, type-safe peripheral access, and compile-time guarantees.

cd sdk/examples/stm32f411-audio  # or stm32wb55, stm32u585
cargo build --release
# Flash via probe-rs: probe-rs run --chip STM32F411CE target/thumbv7em-none-eabihf/release/stm32f411-audio

See sdk/examples/README.md for the full architecture overview and per-target build instructions.

Note: For STM32F103 (Cortex-M3, 20KB RAM), C remains the primary recommendation due to the extremely constrained memory environment and limited Embassy HAL coverage for this target. Use the C SDK with ADPCM or sensor-only mode.


C Examples (alternative) ### C Example: STM32F411 Audio Streaming ### 1. Implement Platform Callbacks
// platform_stm32f411.c
#include "xylolabs/client.h"
#include "stm32f4xx_hal.h"

static UART_HandleTypeDef *s_modem_uart;
static I2S_HandleTypeDef  *s_i2s;

static xylolabs_err_t stm32_tcp_connect(const char *host, uint16_t port) {
    // Send AT+CIPOPEN to LTE modem over s_modem_uart
    char cmd[128];
    snprintf(cmd, sizeof(cmd), "AT+CIPOPEN=0,\"TCP\",\"%s\",%u\r\n", host, port);
    HAL_UART_Transmit(s_modem_uart, (uint8_t *)cmd, strlen(cmd), 1000);
    // Wait for modem OK response...
    return XYLOLABS_OK;
}

static xylolabs_err_t stm32_tcp_send(const uint8_t *data, size_t len) {
    // AT+CIPSEND then raw data
    HAL_UART_Transmit(s_modem_uart, (uint8_t *)data, len, 5000);
    return XYLOLABS_OK;
}

static uint64_t stm32_get_time_us(void) {
    return (uint64_t)HAL_GetTick() * 1000;
}

static void stm32_watchdog_feed(void) {
    HAL_IWDG_Refresh(&hiwdg);
}

const xylolabs_platform_t g_platform = {
    .tcp_connect      = stm32_tcp_connect,
    .tcp_send         = stm32_tcp_send,
    .tcp_recv         = stm32_tcp_recv,
    .tcp_close        = stm32_tcp_close,
    .tcp_is_connected = stm32_tcp_is_connected,
    .get_time_us      = stm32_get_time_us,
    .sleep_ms         = stm32_sleep_ms,
    .watchdog_feed    = stm32_watchdog_feed,
};
### 2. Main Loop
// Rust (Embassy) - Recommended
use xylolabs_sdk::{XylolabsClient, Config, StreamDef};
use xylolabs_hal_stm32::Stm32Platform;
use embassy_stm32::i2s::{I2S, Config as I2sConfig};

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let platform = Stm32Platform::new(/* modem uart, watchdog */);
    let mut client = XylolabsClient::new(platform, Config::default());

    client.set_api_key("xk_your_key_here");
    client.set_device_id(1);

    let streams = [
        StreamDef::new(0, "vibration_x", ValueType::F32, "g", 100.0),
        StreamDef::new(1, "temperature", ValueType::F32, "celsius", 1.0),
    ];
    client.session_create("factory_floor_1", &streams).unwrap();

    loop {
        let samples = i2s_read(&mut i2s, &mut buf).await;
        client.audio_feed(samples).unwrap();
        client.meta_feed_f32(0, read_accel_x().await).unwrap();
        client.meta_feed_f32(1, read_temp().await).unwrap();
        client.tick().unwrap();
    }
}
Legacy C equivalent
// main.c
#include "xylolabs/client.h"
#include "platform_stm32f411.h"

static int16_t s_i2s_buf[256 * 4];  // 256 samples x 4ch interleaved

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_I2S2_Init();
    MX_UART2_Init();
    MX_IWDG_Init();

    xylolabs_init(&g_platform);
    xylolabs_set_api_key("xk_your_key_here");
    xylolabs_set_device_id(1);

    xylolabs_stream_def_t streams[] = {
        { 0, "vibration_x", XMBP_VT_F32, "g", 100.0f },
        { 1, "temperature",  XMBP_VT_F32, "celsius", 1.0f },
    };
    xylolabs_session_create("factory_floor_1", streams, 2);

    while (1) {
        // DMA-fill s_i2s_buf from I2S peripheral
        HAL_I2S_Receive_DMA(&hi2s2, (uint16_t *)s_i2s_buf, 256 * 4);
        xylolabs_audio_feed(s_i2s_buf, 256);

        // Feed sensor data
        xylolabs_meta_feed_f32(0, read_accelerometer_x());
        xylolabs_meta_feed_f32(1, read_temperature_bme280());

        xylolabs_tick();
    }
}
--- ## Power Management ### STOP Mode Between Batches The SDK sends audio every 500 ms and metadata every 1000 ms. Between sends, the MCU can enter STOP mode:
// Rust (Embassy) - Recommended
use embassy_stm32::low_power::{stop_with_rtc, Executor};

// Embassy automatically enters STOP mode between async task polls.
// After xylolabs tick, the executor sleeps until the next wakeup event.
loop {
    client.tick().await;
    // Embassy enters STOP mode here if no work is pending;
    // RTC alarm or EXTI wakeup restores clocks automatically.
    Timer::after_millis(10).await;
}
Legacy C equivalent
// After xylolabs_tick(), check if next send is far away
uint32_t next_send_ms = xylolabs_next_send_ms();  // SDK helper
if (next_send_ms > 50) {
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    // RTC wakeup or EXTI restores clock and resumes
    SystemClock_Config();  // Restore PLL after STOP
}
### IWDG Configuration The SDK feeds the watchdog on every `xylolabs_tick()` call. Configure IWDG with an 8-second timeout:
// Rust (Embassy) - Recommended
use embassy_stm32::wdg::IndependentWatchdog;

// Configure IWDG with 8-second timeout
let mut wdg = IndependentWatchdog::new(p.IWDG, 8_000_000); // 8s in microseconds
wdg.unleash();
// SDK feeds watchdog on every tick
wdg.pet();
Legacy C equivalent
// CubeMX IWDG config:
// Prescaler: IWDG_PRESCALER_256
// Reload: 0xFFF  → ~32s on F411 (LSI ~32kHz / 256 / 4096)
// Tune to match XYLOLABS_WATCHDOG_TIMEOUT_MS
### RTC Wakeup Timer Use RTC wakeup to exit STOP mode at precise intervals:
// Rust (Embassy) - Recommended
use embassy_stm32::rtc::{Rtc, RtcConfig};

// Embassy's low-power executor uses RTC wakeup automatically.
// Configure RTC at init:
let rtc = Rtc::new(p.RTC, RtcConfig::default());
// The executor sets RTC alarms to wake from STOP at precise intervals.
Legacy C equivalent
HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 1, RTC_WAKEUPCLOCK_CK_SPRE_16BITS);
// ISR exits STOP, xylolabs_tick() runs, then re-enters STOP
--- ## LTE-M1 Modem Integration The SDK communicates with LTE modems over UART using AT commands. Tested modems: | Modem | Interface | Notes | |-------|-----------|-------| | BG770A | UART | Cat-M1/NB-IoT; supports PSM and eDRX | | BC660K | UART | NB-IoT focused | ### Modem Init Sequence (BG770A example)
// Rust (Embassy) - Recommended
use embassy_stm32::usart::{Uart, Config as UartConfig};

// AT command sequence for TCP socket setup
async fn modem_init(uart: &mut Uart<'static>) {
    let init_cmds = [
        ("AT\r\n", "OK"),                          // Check modem alive
        ("AT+CMEE=2\r\n", "OK"),                   // Enable verbose error codes
        ("AT+CNCFG=0,1,\"your.apn\"\r\n", "OK"),  // Configure APN
        ("AT+CNACT=0,1\r\n", "OK"),                // Activate PDP context
    ];
    for (cmd, expect) in init_cmds {
        at_send_expect(uart, cmd, expect).await.unwrap();
    }
}
Legacy C equivalent
// AT command sequence for TCP socket setup
static const char *s_init_cmds[] = {
    "AT\r\n",                          // Check modem alive
    "AT+CMEE=2\r\n",                   // Enable verbose error codes
    "AT+CNCFG=0,1,\"your.apn\"\r\n",  // Configure APN
    "AT+CNACT=0,1\r\n",               // Activate PDP context
    NULL
};
### PSM (Power Saving Mode) for BG770A
// Rust (Embassy) - Recommended
// Enable PSM: T3412=1h, T3324=10s active window
at_send_expect(uart, "AT+CPSMS=1,,,\"00100001\",\"00000001\"\r\n", "OK").await.unwrap();
// Modem wakes on schedule, SDK connects and sends batch, modem sleeps
Legacy C equivalent
// Enable PSM: T3412=1h, T3324=10s active window
AT+CPSMS=1,,,"00100001","00000001"
// Modem wakes on schedule, SDK connects and sends batch, modem sleeps
--- ## Pinout Reference For detailed pinout diagrams, refer to the SVG diagrams: - `docs/diagrams/stm32f411-pinout.svg` — Black Pill I2S + UART + I2C/SPI connections - `docs/diagrams/stm32f103-pinout.svg` — Blue Pill sensor-only wiring --- ## Troubleshooting | Symptom | Likely cause | Fix | |---------|-------------|-----| | Hardfault on init | Stack overflow (F103) | Reduce `XYLOLABS_XMBP_BUF_SIZE` and `XYLOLABS_AUDIO_RING_SIZE` | | Audio quality poor | I2S clock mismatch | Verify I2S prescaler matches `XYLOLABS_AUDIO_ORIGINAL_RATE` (96kHz) | | Modem timeout | UART DMA conflict | Use interrupt mode or separate DMA channel for modem vs. I2S | | XAP encode hangs | No FPU on F103 | Set `XYLOLABS_ENABLE_ADPCM=1` or use sensor-only mode | | High latency | STOP mode too aggressive | Increase wakeup frequency or disable STOP for audio targets |