BLE telemetry¶
Goal. Same behaviour as the USB link, over Bluetooth Low Energy:
- the PC receives the same STATE, INIT and STATUS frames;
- the same viewers, MATLAB and Python, show the attitude;
- the same
.matand.binlogs are written.
The USB link keeps working unchanged. REPLAY and the embedded-vs-reference comparison (Validation against the reference) stay USB-only. The GATT service is designed so that a mobile app can use it as it is.
Status:
| Item | State |
|---|---|
| Notifications on the board | working, checked with nRF Connect (frames starting with A5 5A) |
| PC over BLE (Python) | working on the board: 57 STATE/s, lag flat within ±0.02 s over 30 s, 0 lost, 0 firmware drops (Verification) |
| PC over BLE (MATLAB) | same frame path; tested with simulated notifications |
Architecture¶
flowchart LR
IMU["ImuTask<br/>(Realtime, never touches BLE)"] -- qTlm --> TT["TelemetryTask<br/>tx_frame()"]
TT -- "all frames" --> UART["UART DMA → ST-LINK VCP"]
TT -- "STATE decimated, INIT, STATUS<br/>(no REPLAY)" --> LO[("LO ring 2 KB")]
AT["AppTask<br/>protocol v2"] -- "ACK, EVENT, CAPS, PARAM,<br/>CALIB, CHECKS, FLIGHT…" --> HI[("HI ring 2 KB")]
HI --> TX["BleTxThread<br/>every 10 ms or on TX_POOL_AVAILABLE:<br/>peek ≤ ATT_MTU−3 bytes, HI first"]
LO --> TX
TX -- "aci_gatt_update_char_value()" --> IPCC["IPCC"] --> M0[["Cortex-M0+<br/>BLE stack"]] --> RF(("radio"))
Phase 2 had one ring. Protocol v2 added the HI ring and its admission levels (see ACwing Pilot protocol v2), so that ACKs and events never wait behind the attitude stream.
| File | Role |
|---|---|
App/telemetry/tlm_ble.c/.h |
BLE queues (HI and LO lock-free rings), link state, STATE decimation, commands. No HAL, no BLE stack: host-testable |
App/bsp/ble_tlm_svc.c/.h |
GATT service on STM32_WPAN, event handler, TX thread |
STM32_WPAN/App/app_ble.c |
CubeMX-generated BLE application; three one-line hooks in USER CODE sections (Firmware hooks) |
Core/Src/app_entry.c, hw_timerserver.c, … |
CubeMX-generated infrastructure of the radio core (IPCC, timer server, system commands) |
Design choices:
- Byte stream, not packets. A notification carries up to ATT_MTU−3 bytes
of the frame stream and may cut frames anywhere. The receiver concatenates
notifications and re-synchronises on
A5 5A+ CRC with the usual parser. It works with any MTU (23 by default, 185 on iOS, up to 247 on PCs) without a fragmentation protocol. Only the first notification after subscribing is guaranteed to start at a frame boundary. - ImuTask is never involved. The BLE work is in TelemetryTask (queueing) and BleTxThread (sending).
- Flow control. When the link is slower than the data,
aci_gatt_update_char_value()returnsBLE_STATUS_INSUFFICIENT_RESOURCES. The chunk is kept and retried onACI_GATT_TX_POOL_AVAILABLE. If the ring fills up, whole frames are dropped and counted inSTATUS.ble_drop. - Thread safety. In the FreeRTOS version of
app_ble.c,BLE_StatusNot()takesMtxHciIdaround every HCI command, so ACI calls from BleTxThread are serialised with the stack's. The GATT event handler, which runs in the stack's thread, only sets flags.
GATT service¶
| Item | UUID | Properties | Content |
|---|---|---|---|
| Service "ACwing telemetry" | 7E4A0001-2C6B-4F8E-9A5D-3B1C0E1D2F30 |
primary | |
| TLM | 7E4A0002-… |
Notify | frame byte stream, ≤ ATT_MTU−3 bytes per notification |
| CMD | 7E4A0003-… |
Write, Write w/o response | [0x01] resend INIT · [0x02, N] STATE every N acc updates (1…240) |
When a client enables notifications, the board:
- clears the queues;
- sends CAPS, INIT and CHECKS before any periodic frame (protocol v2);
- requests a 15–30 ms connection interval, a 4 s supervision timeout, an MTU exchange and 251-byte link-layer packets.
These values are within the iOS rules and are accepted by Windows, macOS and Linux. The central has the last word.
Bandwidth¶
| Frame | Size on air | Rate over BLE | Bytes/s |
|---|---|---|---|
| STATE | 77 B | 60 Hz (APP_BLE_STATE_DECIM = 4) |
4620 |
| STATUS | 68 B | 10 Hz | 680 |
| INIT | 143 B | ~1 Hz, plus once on subscribe | ~143 |
| Total | ~5.4 kB/s |
- With MTU 23 (20 B per notification), ~270 notifications/s. If
ble_dropgrows, use decimation 8 (30 Hz). - With MTU ≥ 185, one notification carries 2–3 frames.
CubeMX configuration¶
The .ioc in the repository already has the BLE middleware enabled. For
reference, and to check after a regeneration:
- Clocks.
- HSE 32 MHz and LSE 32.768 kHz crystals on.
- RTC clock and RF wake-up clock from LSE.
- SYSCLK 64 MHz from the PLL fed by HSE (M = /2, N = ×8, R = /2). The crystal also makes the UART baud rate accurate.
- Peripherals.
- HSEM and IPCC activated, with the IPCC C1 RX/TX interrupts.
- RTC: clock source activated, internal wake-up (timer server of the stack).
- RNG and RF activated.
- STM32_WPAN.
- BLE enabled, server profile, Heart Rate template. It is kept
because, in the CubeMX version used, it is the template that exposes the
advertised name (
LOCAL_NAME); with the Custom Template the field is not available. The Heart Rate service stays registered alongside ours, unused. - The generated
hrs_app.chas three unused functions (compiler warnings). They are outside USER CODE, so silence them for that file only: Properties → C/C++ Build → Settings → MCU GCC Compiler → Miscellaneous, add-Wno-unused-function -Wno-unused-variable(Debug and Release). - Our service is added in code (
ble_tlm_svc.c), not in the BLE GATT tab. - Debug trace off: it would use USART1, our telemetry port.
- Low power off (
CFG_LPM_SUPPORTED= 0): stop mode would halt the I²C/DMA of the 960 Hz loop.
- BLE enabled, server profile, Heart Rate template. It is kept
because, in the CubeMX version used, it is the template that exposes the
advertised name (
- FreeRTOS. Heap ≥ 48 KB (52000 in the repository): the stack's threads take ~10 KB. Tickless idle off.
-
NVIC. Every interrupt that calls RTOS functions needs priority ≥ 5 (IMU EXTI, IPCC, HSEM, RTC wake-up).
-
BLE parameters (STM32_WPAN → BLE → Configuration).
- Advertised name:
ACwFC(ACwing FC). CubeMX accepts only 5 characters here because the name shares the 31-byte advertising packet with the template's data. The same name is used as the GAP device name. The PC tools recognise any name starting withACw, and alsoIEKF…(firmware before the ACwing rename) andSTM32WB(the CubeMX default). CFG_BLE_MAX_ATT_MTU: 247. It gives fewer and fuller notifications.
- Advertised name:
The CubeIDE project is named ACwing_FC_FW (ACwing_FC_FW.ioc,
ACwing_FC_FW.launch).
Firmware hooks¶
STM32_WPAN/App/app_ble.c has three calls, all in USER CODE sections, so they
survive regeneration:
/* USER CODE BEGIN Includes */
#include "ble_tlm_svc.h"
/* APP_BLE_Init(): after SVCCTL_Init(), before advertising starts */
/* USER CODE BEGIN APP_BLE_Init_3 */
ble_tlm_svc_init();
/* SVCCTL_App_Notification(), case HCI_LE_CONNECTION_COMPLETE_SUBEVT_CODE */
/* USER CODE BEGIN HCI_EVT_LE_CONN_COMPLETE */
ble_tlm_on_connect(p_connection_complete_event->Connection_Handle);
/* SVCCTL_App_Notification(), case HCI_DISCONNECTION_COMPLETE_EVT_CODE */
/* USER CODE BEGIN EVT_DISCONN_COMPLETE */
ble_tlm_on_disconnect();
APP_TLM_BLE in app_config.h switches the BLE path:
- 1 (repository default): the BLE path is active;
- 0: everything compiles without using the stack, and STATUS reports
ble_state= 0.
Wireless stack on the Cortex-M0+¶
The radio core runs a binary from ST that is flashed separately with STM32CubeProgrammer:
- Take its version from the same STM32CubeWB package that CubeMX uses: a mismatch is the most common reason why CPU2 never starts.
- The files are in
Projects/STM32WB_Copro_Wireless_Binaries/STM32WB5x/of that package:- the stack,
stm32wb5x_BLE_Stack_full_fw.bin; - the FUS, if it is older than the stack requires.
- the stack,
- In Firmware Upgrade Services:
- Start FUS, Read FUS infos. Update the FUS first if needed.
- Delete FW.
- Firmware Upgrade with the stack at the start address given in
Release_Notes.htmlfor STM32WB5xxG (1 MB). It changes from version to version. - Start Wireless Stack.
- The application uses at most 512 KB of flash (
STM32WB55RGVX_FLASH.ld), below the stack.
Bring-up checklist¶
- USB viewer first. The text panel shows
BLE: advertising: the service is registered and the stack is running.- No
BLEline: the stack did not start. Check the CPU2 firmware (Wireless stack on the Cortex-M0+). err 0x..: last stack error.0x64/0x98at start-up means too little GATT attribute space.
- No
- Phone, with nRF Connect or LightBlue.
- Scan without filters: scanner filters apply to the advertising packet, never to the notifications.
- Connect and enable notifications on
7E4A0002-…: the data starts withA5 5A. - The USB viewer now shows
BLE: streaming, the MTU anddrop 0.
- 960 Hz loop.
s_irqCount(~480/s) ands_procCycMaxin Live Expressions must behave as without BLE. - Disconnect the phone, then connect from the PC (PC tools over BLE). A peripheral accepts one central at a time.
PC tools over BLE¶
MATLAB (R2019b+, Windows 10+/macOS, Bluetooth 4.0+ adapter):
L = acwingLiveViewer("ble"); % board found automatically, STATE 60 Hz
L = acwingLiveViewer("ble", "ACwFC", 8); % by name or address, STATE 30 Hz
blelist % what is advertising
Python (pip install bleak, or pip install -e ".[ble]"; Windows, macOS,
Linux/BlueZ):
acwing-tools scan # boards advertising, marked "<-- ACwing board"
acwing-tools viewer --ble # board found automatically, STATE 60 Hz
acwing-tools viewer --ble ACwFC --decim 8
acwing-tools capture --ble --duration 30 # no graphics: received rates every 2 s
How the tools behave:
- Discovery. It picks the strongest device whose name starts with
ACw(ACwFC). Boards with older firmware are found too: names starting withIEKF, orSTM32WB(the CubeMX default). - On connection. The tools subscribe to TLM and write the decimation to CMD.
- Processing. Notifications are appended as bytes and go through the same
parser as the serial port. Log,
.binand panel are identical to USB; the panel adds aBLE:line with state, MTU, drops and notifications received. lost. It counts only real gaps of the decimated stream.
Verification¶
On the board. The link was measured with acwing-tools capture --ble --duration 30 and
acwing-tools info, on the P-NUCLEO-WB55, HSE clock, Windows PC:
| Quantity | Measured |
|---|---|
| STATE received | 56.8/s; 916 Hz IMU / 16 (the LSM6DSV16X runs at 916 Hz, not 960) |
| Notifications | ~62/s, ~5.2 kB/s, ATT MTU 247 |
| Lag PC − firmware time | flat, −0.00 … +0.02 s over 30 s |
| Lost STATE, firmware BLE drops | 0, 0 |
| Firmware | 916 gyro samples/s, ~560 IMU interrupts/s, 0 FIFO overruns, 0 I²C errors; filter time 30.0 s over 29.9 s of uptime |
Two firmware problems appeared only on the board, with the BLE stack running,
and were fixed in ImuTask (Troubleshooting):
- INT1 stuck high. It caused 0 IRQ/s, 39 % of the samples and ~50 I²C errors/s.
- Gyro integrated partly with the nominal period. The filter time fell 1.3 % behind real time.
Off the board:
| Check | Result |
|---|---|
| Notifications on the board, nRF Connect | frames A5 5A … received |
Full firmware build with ARM GCC 13.3, include paths and defines read from .cproject (87 C files + startup, linked with STM32WB55RGVX_FLASH.ld and CMSIS-DSP) |
0 errors, 0 warnings (with the per-file flags of hrs_app.c, CubeMX configuration). HSE clock, MTU 247. Debug flags (-O0 -g3): 108 KB flash, 73 KB RAM; -O2: 66 KB flash |
ble_tlm_svc.c against the STM32_WPAN headers |
all ACI/HCI functions, events and structs used exist with that signature |
| Host test of the BLE queue: 60 s of frames, random MTU 23–247, 30 % refused sends | bytes received = frames queued; overload drops whole frames only |
| MATLAB viewer, BLE path with simulated notifications (Octave) | 660 STATE at step 4, 0 lost, INIT/STATUS decoded, .bin identical, CMD [2 4] written |
Python: 7 BLE tests (tests/test_ble.py, 26 in the whole suite) |
STATUS 54/62 B, board discovery, reassembly from 20-byte notifications, viewer on a decimated stream (0 lost, BLE line), a real gap counted once, BLE worker process, headless capture --ble |
Troubleshooting (BLE)¶
| Symptom | Cause / action |
|---|---|
| The phone does not list the board | scanner filter on data: remove it (Bring-up checklist); reset the board (after 60 s the advertising interval grows to 1–2.5 s) |
No BLE line in the USB viewer |
CPU2 stack missing or not matching the CubeWB package (Wireless stack on the Cortex-M0+) |
| USB telemetry garbled after enabling BLE | stack debug trace on USART1: trace off |
| SWD lost, IMU stops | low power on: CFG_LPM_SUPPORTED = 0, tickless idle off |
| HardFault / stack overflow at start-up | FreeRTOS heap too small |
multiple definition of HAL_GPIO_EXTI_Callback after a regeneration |
keep a single definition, calling app_exti(pin) |
ble_drop grows |
link slower than the data: decimation 8, larger MTU |
PC viewer over BLE: rate below 60 Hz, lag growing, lost 0 and drop 0 |
the PC delivers the notifications late (nothing is lost on the link). The Python tools receive BLE in a separate process for this reason (since the BLE support was added); update them. The BLE: line shows the notifications/s actually received, draw the redraw time. acwing-tools capture --ble measures the link without graphics: if it shows 60 STATE/s and a stable lag, the limit is the viewer's redraw on that PC |
| PC tools: board not found | it is connected to another central (phone): disconnect it. Python on Linux: bluetoothd running |
The mobile app¶
The service was designed to be what a mobile app needs:
- 128-bit UUIDs;
- notify plus write-without-response;
- connection parameters within Apple's rules;
- an MTU-agnostic byte stream.
The ACwing Pilot app (02 - Tools/02 - App) uses it as it is. It parses
the same frame stream, reads STATE for the attitude and STATUS for the BLE
diagnostics, and adds the frames and commands of
ACwing Pilot protocol v2 on the same two characteristics.