BLE
On the wireless Core, core_ble is a peripheral-mode Bluetooth Low Energy API: advertise a name, define a GATT server with read / write / notify characteristics, and react to connections — all in a few C calls over the ST radio stack.
Overview
"ble": { "enabled": true } in config.json (the Makefile links the stack from it) and a clock of medium or higher. The C API is Tier 1 (handle-based, C-callback driven). You do not have to write it by hand: a project can declare its GATT contract instead and have the builder, a typed API, and the publishing generated for it.The shape of a BLE app: register a service builder before init, bring up the stack, advertise, then pump core_ble_process() from your main loop so the radio gets serviced. core.h doesn’t pull in BLE, so include core_ble.h yourself:
#include "core.h"
#include "core_ble.h"
#include "core_watchdog.h"
void app_services(void); // builder, defined below
int main(void) {
core_init();
core_led_init();
core_ble_set_services(app_services);
core_ble_enable_pairing(); // optional: Just Works pairing + bonding
core_ble_init();
core_ble_process(); // one pass before advertising
core_ble_advertise("Core.ST.W5");
while (1) {
core_ble_process(); // must run continuously
core_watchdog_feed();
}
}Advertising
Advertising is what makes the device discoverable. Start and stop it by name, and tune the interval and transmit power to trade discovery latency against battery:
// before core_ble_init():
core_ble_set_adv_interval(100, 150); // min/max ms (20–10240)
core_ble_set_tx_power(1); // 0 low (-20 dBm), 1 medium (0 dBm), 2 high (+10 dBm)
// after init:
core_ble_advertise("Core.ST.W5");
// ... core_ble_stop_advertise();Both settings are read when the stack starts, so call them before core_ble_init(). The ble block in config.json can set them too (tx_power, adv_interval_ms, name).
GATT server
A GATT server is services, each holding characteristics — the values a phone or hub reads, writes, or subscribes to. Build them in your service-builder function. The names are for your own reading; core_ble_add_service() and core_ble_add_char() hand out UUIDs sequentially, in registration order:
static core_ble_char_t led_char, count_char;
static void on_led_write(const uint8_t *data, uint16_t len, void *ctx) {
(void)ctx;
if (len && data[0]) core_led_on(); else core_led_off();
}
void app_services(void) {
core_ble_svc_t led = core_ble_add_service("LED Control");
led_char = core_ble_add_char(led, "LED State", CORE_BLE_RW, CORE_BLE_BOOL,
on_led_write, NULL);
core_ble_svc_t ctr = core_ble_add_service("Counter");
count_char = core_ble_add_char(ctr, "Count", CORE_BLE_READ | CORE_BLE_NOTIFY,
CORE_BLE_UINT8, NULL, NULL);
}core_ble_add_service_id() / core_ble_add_char_id() to fix a 16-bit id in the SDK’s base UUID, or the _sig variants for a Bluetooth SIG UUID (Battery Level, Device Information).Push a new value with core_ble_set_value. For a notify characteristic it also sends the value to a subscribed central, in the same call. It returns -1 when the stack’s TX queue is full: keep the value and try again on a later pass.
uint8_t n = ++counter;
if (core_ble_set_value(count_char, &n, 1) != 0) {
// queue full: send it again on the next loop pass
}Declaring a contract
Writing the builder by hand is the escape hatch, not the default. A project can declare its services and characteristics in config.json and have coregen generate ble_contract.c: the builder above, a typed ble_<name>_set() per value, and a weak ble_<name>_on_write() you override to receive writes.
"ble": {
"enabled": true,
"name": "My Board",
"contract": [
{ "name": "Counter", "id": "0xFE40",
"characteristics": [
{ "name": "Count", "id": "0xFE41", "type": "uint16",
"access": ["read", "notify"],
"source": { "var": "count" } }
] }
]
}Ids are declared, never generated, so inserting a service cannot renumber the ones after it and break a client you have already shipped. Use "sig" instead of "id" for a Bluetooth SIG UUID that generic scanners can name on sight.
The source field is the part that saves the most work. Bind a characteristic to a variable and it publishes itself: assign count anywhere in your program and the new value reaches the phone, with nothing added to your loop.
#include "core.h"
#include "core_ble.h"
#include "ble_contract.h" /* generated: BLE_DEVICE_NAME, ble_<name>_set() */
#include "core_watchdog.h"
int count; /* not static — the generated publisher reads it */
int main(void) {
core_init(); /* registers the generated contract builder */
core_ble_init();
core_ble_advertise(BLE_DEVICE_NAME);
while (1) {
core_ble_process(); /* publishes bound values as a side effect */
core_watchdog_feed();
count++;
core_delay_ms(100);
}
}"publish": { "hz": 1 } on the characteristic to change the rate. In hand-written code, core_ble_subscribed() answers the same question so you can gate your own publishing the same way.Leave source out and you get the setter and nothing else, which is the hand-written path above. Say "source": "code" to say you meant it, and coregen stops warning that nothing publishes the value.
Connections
Poll core_ble_connected(), or register callbacks to react the moment a central connects or drops:
static void on_connect(void *ctx) { core_led_on(); }
static void on_disconnect(void *ctx) { core_led_off(); }
core_ble_on_connect(on_connect, NULL);
core_ble_on_disconnect(on_disconnect, NULL);Pairing is optional Just Works — call core_ble_enable_pairing() before init and the phone shows a one-time pair prompt. The bond is kept in flash, so it survives resets and the phone reconnects without pairing again. Once it’s on, every characteristic needs an encrypted link. Not there yet: LE Secure Connections (pairing is legacy Just Works) and central/scanner mode.
Also in BLE builds
- BLE and Thread in one image. Enable both in
config.jsonand the radio is time-shared.core_ble_init()then starts Thread too, so call it and keepcore_ble_process()in the loop. See Thread. - Large notifications by default. After each connection the Core asks for a 251-byte ATT MTU itself, so a central that never asks (iOS, Web Bluetooth) still gets more than 20 bytes per notification.
core_ble_set_auto_mtu(0)turns it off. - Firmware updates over BLE. Every BLE build carries the Studio Link service, which lets Studio install a new image over the air. See Bootloading.
Cross-architecture support
BLE is hardware-verified on the wireless Core; the other ST Cores have no radio. The Nordic (nRF54) family will bring BLE to a second architecture behind the same intent.
See the implementation status for the full matrix.
Known gaps
What the SDK itself lists as missing here, straight from the header:
DSL coverage landed: a project declares a contract, gets palette blocks to publish and receive, and can bind a characteristic to a variable so coregen generates the publisher. What is still missing is the simulator side, so a DSL program that uses BLE cannot be exercised without flashing a board.
core_ble_on_connect() and core_ble_on_disconnect() take a callback pointer rather than overriding a weak symbol, so coregen has no seam to emit a dispatcher against. Every other event in the DSL arrives through that weak-symbol pipeline. Until this is wired the only way to notice a connection from the DSL is to poll.
A `bytes` characteristic can be declared and published from C, but the DSL has no type that carries a buffer and a length, so it gets neither a publish block nor a write handler, and it cannot be bound to a variable. Scalars and strings both work.
Peripheral-only today. No scanning, no central-role connections, no GATT-client reads/writes. Tracked as its own initiative — most Bergsonne use-cases are peripheral-role (sensor advertising to a phone). Apps that need central-role drop into the WBA BLE stack.
core_ble_enable_pairing() does Just Works *legacy* pairing with bonding: characteristics require an encrypted link and the keys persist in flash NVM, so a bonded host reconnects without re-prompting. Not yet exposed: LE Secure Connections (numeric-comparison / passkey MITM protection) and directed advertising for fast reconnect to a known bonded central.
Narrower than it used to be. A contract pins its own 16-bit ids, custom (placed in the Bergsonne base UUID) or SIG-adopted, and core_ble_add_service_id() / _sig() take them verbatim, so ids never shift and deployed clients keep working. What is still missing is a fully arbitrary 128-bit UUID, which is what interop with an existing app that expects some other vendor's base requires.
The WBA radio supports LE Audio (LC3), extended advertising / 2M PHY / coded PHY, and multi-link (multiple simultaneous connections). None of that is exposed.
From the @studio unsupported notes in core_ble.h — tiles@217a38f.
API reference
void core_ble_set_services(void);int core_ble_add_services(void);void core_ble_init(void);int core_ble_advertise(const char * name);int core_ble_stop_advertise(void);void core_ble_process(void);core_ble_svc_t core_ble_add_service(const char * name);core_ble_svc_t core_ble_add_service_id(const char * name, uint16_t id);core_ble_char_t core_ble_add_char(core_ble_svc_t svc, const char * name, uint8_t access, uint8_t type, core_ble_write_cb on_write, void * ctx);core_ble_char_t core_ble_add_char_id(core_ble_svc_t svc, const char * name, uint16_t id, uint8_t access, uint8_t type, core_ble_write_cb on_write, void * ctx);core_ble_svc_t core_ble_add_service_sig(const char * name, uint16_t uuid16);core_ble_char_t core_ble_add_char_sig(core_ble_svc_t svc, const char * name, uint16_t uuid16, uint8_t access, uint8_t type, core_ble_write_cb on_write, void * ctx);int core_ble_set_value(core_ble_char_t ch, const void * data, uint16_t len);int core_ble_notify(core_ble_char_t ch);int core_ble_subscribed(core_ble_char_t ch);int core_ble_connected(void);void core_ble_on_connect(void * ctx);void core_ble_on_disconnect(void * ctx);void core_ble_set_tx_power(uint8_t level);void core_ble_set_adv_interval(uint16_t min_ms, uint16_t max_ms);void core_ble_set_conn_params(uint16_t min_ms, uint16_t max_ms, uint16_t latency, uint16_t timeout_ms);void core_ble_set_auto_mtu(uint8_t on);void core_ble_enable_pairing(void);| Type | Kind | Description |
|---|---|---|
core_ble_svc_t | typedef | Service handle, returned by core_ble_add_service*().typedef uint16_t core_ble_svc_t; |
core_ble_char_t | typedef | Characteristic handle, returned by core_ble_add_char*().typedef uint16_t core_ble_char_t; |
core_ble_write_cb | callback | Write callback: called when the central writes to a characteristic.typedef void (*core_ble_write_cb)(const uint8_t *data, uint16_t len, void *ctx); |
Generated from core_ble.h — tiles@77fd3f3.

