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 |
Rust Build (Recommended)¶
# 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¶
- Create a new STM32 project for your target (F411CE or F103C8).
- Enable I2S, UART2, I2C1, SPI1 peripherals in CubeMX.
- Add the SDK as a linked folder:
- Right-click project → Properties → C/C++ General → Paths and Symbols
- Add
sdk/c/common/includeto include paths - Add
sdk/c/common/srcandsdk/c/stm32/srcsource folders - Add compile definitions in project properties: For F103 (ADPCM legacy path):
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
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
Rust (Recommended for F411 / WB55)¶
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,
};
// 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();
}
}
// 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
}
// 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
// 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
// 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();
}
}