7 Bluetooth Protocol Stack and GATT
Key Concepts
- BLE Stack Layers: Radio → Link Layer → HCI → L2CAP → ATT → GATT/SMP/GAP → Application; each layer adds a header consuming bandwidth
- Link Layer (LL): BLE’s lowest software layer managing channel hopping, packet formatting, CRC, whitening, and connection-state machine
- SMP (Security Manager Protocol): BLE layer on L2CAP CID 0x0006 handling pairing, key distribution, and encrypted communication setup
- GAP Security Mode 1: Encryption-based security; Level 1=no security, Level 2=unauthenticated pairing, Level 3=authenticated pairing, Level 4=authenticated LE Secure Connections
- LE Secure Connections (LESC): BLE 4.2+ pairing method using ECDH (Elliptic Curve Diffie-Hellman) key exchange; provides protection against passive eavesdropping
- PDU (Protocol Data Unit): The unit of data at each stack layer; LL PDU maximum is 27 bytes payload by default (BLE 4.x), extendable to 251 bytes with DLE
- RFCOMM: Serial Port Profile emulation protocol running over L2CAP in Classic Bluetooth, providing virtual COM port functionality
- LE Isochronous Channels: BLE 5.2 feature providing time-bounded, synchronized audio/data delivery; basis for LE Audio and Auracast broadcast audio
Minimum Viable Understanding
The BLE protocol stack uses GATT (Generic Attribute Profile) to organize data into a hierarchy of Services (groups of related data), Characteristics (individual data values with read/write/notify properties), and Descriptors (metadata). Data can be pushed from server to client via Notifications (fast, unconfirmed) or Indications (slower, confirmed delivery). Standard Bluetooth SIG services (Heart Rate, Battery, Environmental Sensing) ensure interoperability, while custom 128-bit UUIDs support proprietary IoT applications.
7.1 Learning Objectives
By the end of this chapter, you will be able to:
- Explain the Bluetooth protocol stack from PHY to application layer, identifying each layer’s role and data-flow responsibility
- Analyze GATT architecture — services, characteristics, and descriptors — and distinguish how ATT and L2CAP underpin it
- Design custom GATT services for IoT sensor applications using 128-bit UUIDs and appropriate characteristic properties
- Compare Notify and Indicate mechanisms, evaluating trade-offs in latency, throughput, and guaranteed delivery for specific use cases
- Implement standard Bluetooth SIG profiles (Heart Rate, Battery, Device Information) for cross-vendor interoperability
- Calculate BLE throughput and battery life given MTU size, connection interval, and peripheral latency parameters
For Beginners: Bluetooth Protocol Stack and GATT
The Bluetooth protocol stack is the layered software that handles communication, from the radio hardware at the bottom to the application at the top. GATT (Generic Attribute Profile) is the layer that defines how BLE devices expose their data as readable and writable characteristics – like a menu that other devices can browse.
Sensor Squad: The Layer Cake of Communication!
“The Bluetooth protocol stack is like a layer cake!” Sammy the Sensor said. “At the very bottom is the radio – that is where the actual wireless signals happen. Then each layer above adds something new, like wrapping a present with paper, then a ribbon, then a bow. By the time my sensor data reaches the top, it is all neatly packaged for the app to use!”
“My favorite layer is GATT,” Lila the LED said. “It organizes everything into Services and Characteristics. Think of it like organizing your school folders – you have a Science folder with a Chemistry notebook inside. GATT has a Heart Rate Service with a Heart Rate Measurement Characteristic inside. Super organized!”
Max the Microcontroller explained, “I handle all these layers at once. The Link Layer manages the connection, L2CAP chops big messages into small packets, ATT reads and writes the actual data, and GATT keeps everything organized. Notifications are my favorite – instead of the phone asking me every second ‘Any new data?’, I just push updates whenever something changes!”
“The whole stack is designed to be lightweight,” Bella the Battery added. “Unlike Classic Bluetooth, which has heavy layers for streaming audio, BLE’s stack is slim and efficient. Less processing means less energy, which means I can keep the whole system running for months on a tiny battery!”
7.2 Prerequisites
Before diving into this chapter, you should be familiar with:
- Bluetooth Overview: Basic understanding of Bluetooth technology
- Bluetooth Connection Establishment: How BLE connections are formed
7.3 Bluetooth Protocol Stack
7.3.1 Classic Bluetooth Stack
The Classic Bluetooth stack is designed for continuous data streaming:
| Layer | Purpose | Key Components |
|---|---|---|
| Application | User applications | Audio, file transfer |
| Profiles | Application behavior | A2DP, HFP, SPP, HID |
| RFCOMM | Serial port emulation | Virtual COM ports |
| L2CAP | Logical link control | Multiplexing, segmentation |
| Link Manager | Connection management | Security, power control |
| Baseband | Packet handling | Piconet, frequency hopping |
| Radio | Physical transmission | 2.4 GHz, GFSK modulation |
7.3.2 BLE Protocol Stack
BLE uses a simplified stack optimized for low power:
| Layer | Purpose | Key Components |
|---|---|---|
| Application | User applications | Sensor data, beacons |
| GATT | Data organization | Services, characteristics |
| ATT | Attribute protocol | Read, write, notify; default MTU 23 bytes (max 517 bytes) |
| L2CAP | Logical link | Fixed channels (CID 0x0004 for ATT), CoC |
| Link Layer | Packets, connections | States: Standby, Advertising, Scanning, Initiating, Connection, Synchronization |
| PHY | Physical layer | LE 1M (1 Mbps), LE 2M (2 Mbps), LE Coded (125/500 kbps long-range) |
7.3.3 Key Differences
| Aspect | Classic Bluetooth | BLE |
|---|---|---|
| Data Model | Stream-based (RFCOMM) | Attribute-based (GATT) |
| Connection | Persistent | Can be transient |
| Profiles | Complex (A2DP, HFP) | Simple (GATT services) |
| Discovery | Profile-specific | Universal GATT discovery |
7.4 GATT Architecture
GATT (Generic Attribute Profile) organizes data like a file system:
7.4.1 Hierarchy
Profile (Use case definition)
|
+-- Service (Group of related data)
|
+-- Characteristic (Individual data point)
|
+-- Value (The actual data)
+-- Descriptor (Metadata, configuration)
7.4.2 Services
Services group related characteristics:
Standard Services (Bluetooth SIG defined):
| UUID | Service | Description |
|---|---|---|
| 0x1800 | Generic Access | Device name, appearance |
| 0x1801 | Generic Attribute | Service change indication |
| 0x180A | Device Information | Manufacturer, model, serial |
| 0x180F | Battery Service | Battery level |
| 0x181A | Environmental Sensing | Temperature, humidity |
| 0x180D | Heart Rate | Heart rate measurement |
Custom Services:
Use 128-bit UUIDs for proprietary data:
// Custom service UUID (generated)
#define CUSTOM_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"7.4.3 Characteristics
Characteristics are individual data values with properties:
Properties:
| Property | Code | Description |
|---|---|---|
| Read | 0x02 | Client can read value |
| Write | 0x08 | Client can write (with response) |
| Write No Response | 0x04 | Client can write (no ack) |
| Notify | 0x10 | Server can push (no ack) |
| Indicate | 0x20 | Server can push (with ack) |
Choosing Properties:
| Use Case | Properties | Example |
|---|---|---|
| On-demand sensor reading | Read | Temperature on request |
| Real-time streaming | Notify | Heart rate updates |
| Critical alerts | Indicate | Low battery warning |
| Configuration | Read + Write | Alarm threshold |
| Commands | Write No Response | LED on/off |
7.4.4 Descriptors
Descriptors provide metadata about characteristics:
Common Descriptors:
| UUID | Name | Purpose |
|---|---|---|
| 0x2900 | Characteristic Extended Properties | Additional properties |
| 0x2901 | Characteristic User Description | Human-readable name |
| 0x2902 | Client Characteristic Configuration | Enable notify/indicate |
| 0x2904 | Characteristic Presentation Format | Units, exponent |
CCCD (0x2902):
The Client Characteristic Configuration Descriptor is critical for notifications:
// Client writes to CCCD to enable/disable notifications
// 0x0000 = Disabled
// 0x0001 = Notifications enabled
// 0x0002 = Indications enabled7.4.5 Knowledge Check: ATT and L2CAP
7.5 Notification vs Indication
Understanding when to use each is crucial:
7.5.1 Notifications (Fire-and-Forget)
- Server pushes data without confirmation
- Lower latency (no round-trip)
- Possible data loss if packet dropped
- Best for: High-frequency telemetry
// Server sends notification
pCharacteristic->setValue(sensorData, sizeof(sensorData));
pCharacteristic->notify(); // No confirmation expected7.5.2 Indications (Confirmed Delivery)
- Server waits for client acknowledgment
- Higher latency (requires ACK)
- Guaranteed delivery (retries if failed)
- Best for: Critical data, configuration
// Server sends indication
pCharacteristic->setValue(alertData, sizeof(alertData));
pCharacteristic->indicate(); // Blocks until ACK received7.5.3 Comparison
| Aspect | Notify | Indicate |
|---|---|---|
| Acknowledgment | No | Yes |
| Latency | Lower | Higher |
| Throughput | Higher | Lower |
| Reliability | Best-effort | Guaranteed |
| Use Case | Sensor streaming | Alerts, config |
7.5.4 Common Pitfall
Pitfall: Misunderstanding GATT Notification vs Indication
The Mistake: Using indications (confirmed) when notifications (unconfirmed) would suffice, or vice versa, leading to either unnecessary latency/overhead or silent data loss.
Why It Happens: Both seem to “push” data from peripheral to central. Developers don’t realize indications require ACK (adding round-trip latency) while notifications are fire-and-forget.
The Fix: Use notifications for high-frequency telemetry where occasional packet loss is acceptable (sensor readings, heart rate). Use indications only for critical data that must be confirmed (configuration changes, alarms). Never use indications for streaming data—the ACK overhead will throttle throughput to a fraction of potential.
7.6 Designing Custom GATT Services
7.6.1 Example: Environmental Sensor
Design a GATT service for a multi-sensor environmental monitor:
Service Definition:
Environmental Monitoring Service (Custom UUID)
|
+-- Temperature Characteristic
| UUID: 0x2A6E (standard)
| Properties: Read, Notify
| Format: sint16 (0.01 degree resolution)
|
+-- Humidity Characteristic
| UUID: 0x2A6F (standard)
| Properties: Read, Notify
| Format: uint16 (0.01% resolution)
|
+-- Pressure Characteristic
| UUID: Custom 128-bit
| Properties: Read, Notify
| Format: uint32 (0.1 Pa resolution)
|
+-- Sampling Rate Characteristic
UUID: Custom 128-bit
Properties: Read, Write
Format: uint16 (seconds)
7.6.2 Implementation
// Service UUIDs
#define ENV_SERVICE_UUID "181A" // Standard Environmental Sensing
#define TEMP_CHAR_UUID "2A6E" // Standard Temperature
#define HUMIDITY_CHAR_UUID "2A6F" // Standard Humidity
#define PRESSURE_CHAR_UUID "custom-uuid-here"
#define SAMPLING_CHAR_UUID "custom-uuid-here"
void setupGATT() {
// Create service
BLEService* pEnvService = pServer->createService(ENV_SERVICE_UUID);
// Temperature (Read + Notify)
pTempChar = pEnvService->createCharacteristic(
TEMP_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
pTempChar->addDescriptor(new BLE2902()); // Enable CCCD
// Humidity (Read + Notify)
pHumidityChar = pEnvService->createCharacteristic(
HUMIDITY_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
pHumidityChar->addDescriptor(new BLE2902());
// Sampling Rate (Read + Write for configuration)
pSamplingChar = pEnvService->createCharacteristic(
SAMPLING_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pEnvService->start();
}7.6.3 Data Formatting
BLE uses little-endian byte order:
// Temperature: 25.50 degrees Celsius
// Format: sint16, resolution 0.01
int16_t tempValue = 2550; // 25.50 * 100
uint8_t tempData[2];
tempData[0] = tempValue & 0xFF; // Low byte first
tempData[1] = (tempValue >> 8) & 0xFF; // High byte second
pTempChar->setValue(tempData, 2);
pTempChar->notify();7.7 Standard Profiles
7.7.1 Heart Rate Service (0x180D)
Used by fitness trackers and medical devices:
Characteristics:
| UUID | Name | Properties |
|---|---|---|
| 0x2A37 | Heart Rate Measurement | Notify |
| 0x2A38 | Body Sensor Location | Read |
| 0x2A39 | Heart Rate Control Point | Write |
Data Format (0x2A37):
Byte 0: Flags
Bit 0: Heart Rate Value Format (0=uint8, 1=uint16)
Bit 1-2: Sensor Contact Status
Bit 3: Energy Expended Present
Bit 4: RR-Interval Present
Byte 1-2: Heart Rate Value (uint8 or uint16)
Byte 3-4: Energy Expended (optional, uint16)
Byte 5+: RR-Intervals (optional, uint16[])
7.7.2 Battery Service (0x180F)
Simple battery level reporting:
Characteristics:
| UUID | Name | Properties |
|---|---|---|
| 0x2A19 | Battery Level | Read, Notify |
Data Format:
Single byte, 0-100 representing percentage.
7.7.3 Device Information Service (0x180A)
Manufacturer and model information:
Characteristics:
| UUID | Name | Content |
|---|---|---|
| 0x2A29 | Manufacturer Name | “Acme Inc” |
| 0x2A24 | Model Number | “Sensor-v1” |
| 0x2A25 | Serial Number | “SN123456” |
| 0x2A26 | Firmware Revision | “1.2.3” |
| 0x2A27 | Hardware Revision | “A” |
| 0x2A28 | Software Revision | “2.0.0” |
7.8 Adaptive Frequency Hopping
Bluetooth uses frequency hopping to avoid interference:
Minimum Viable Understanding: Adaptive Frequency Hopping (AFH)
Core Concept: Bluetooth mitigates 2.4 GHz interference by rapidly hopping between channels (1600 hops/second for Classic, variable for BLE) and adaptively blacklisting channels where interference is detected. Classic Bluetooth uses 79 channels (1 MHz each), while BLE uses 40 channels (2 MHz each) with 37 for data and 3 for advertising.
Why It Matters: The 2.4 GHz ISM band is the most crowded spectrum in IoT, shared by Wi-Fi, Zigbee, microwave ovens, and other Bluetooth devices. Without frequency hopping, a single interferer (like a Wi-Fi access point on channel 6) could completely block communication. AFH typically recovers from Wi-Fi interference within 5-10 seconds by mapping out and avoiding the occupied frequencies.
Key Takeaway: If you experience Bluetooth audio dropouts or BLE sensor disconnections, check for Wi-Fi channel overlap first. Move your Wi-Fi router to channel 1 or 11 (which overlap fewer Bluetooth data channels), or place the Bluetooth central device away from the Wi-Fi access point. For BLE, advertising channels 37, 38, and 39 are specifically positioned between Wi-Fi channels 1, 6, and 11 to ensure discovery works even in congested environments.
7.8.1 Knowledge Check: GATT Hierarchy
7.8.2 Knowledge Check: Notification vs Indication
7.8.3 Knowledge Check: CCCD Configuration
7.9 Real-World Deployment: GATT Design at Scale
7.9.1 Abbott FreeStyle Libre 3: Continuous Glucose Monitoring via BLE GATT
Abbott’s FreeStyle Libre 3 CGM sensor, worn by over 5 million users globally, transmits glucose readings every minute from a coin-cell-powered sensor to a smartphone app via BLE GATT. The GATT service design illustrates production-grade decisions that differ significantly from textbook examples.
GATT service architecture (reverse-engineered from BLE protocol analysis):
Abbott uses a custom GATT service (not the standard Glucose Service 0x1808) because the Bluetooth SIG’s standardized service lacks streaming support and sensor-specific calibration data:
| Characteristic | Properties | Size | Purpose |
|---|---|---|---|
| Glucose reading | Notify | 8 bytes | Current glucose + trend arrow + timestamp offset |
| Historical buffer | Read + Notify | 20 bytes | 8-hour rolling buffer for backfill after BLE disconnection |
| Sensor status | Read + Indicate | 4 bytes | Remaining lifetime, warm-up state, error codes |
| Calibration data | Read | 32 bytes | Factory calibration constants (read once on first connect) |
| Security challenge | Write + Indicate | 16 bytes | AES-128 mutual authentication handshake |
Why Notify for glucose, Indicate for sensor status:
Abbott’s clinical validation data from 14,577 patients showed that occasional missed glucose readings (Notify, no ACK) have zero clinical impact because the next reading arrives in 60 seconds. But a missed sensor-failure alert could leave a patient relying on stale data. The Indicate property for sensor status guarantees delivery of critical state changes, with the trade-off being 12 ms additional latency per indication (ACK round-trip) versus 0.5 ms for a notification.
Connection parameter optimization:
| Parameter | Initial pairing | Normal operation | Background (phone in pocket) |
|---|---|---|---|
| Connection interval | 15 ms | 500 ms | 2000 ms |
| Peripheral latency | 0 | 4 | 19 |
| Supervision timeout | 2 s | 6 s | 20 s |
| Effective wake rate | 66/sec | 1/sec | 0.05/sec (every 20 s) |
| Current draw | 3.2 mA | 48 uA average | 8 uA average |
This adaptive parameter strategy achieves 14.5-day sensor lifetime on a 35 mAh silver-oxide battery. During the initial pairing phase (30 seconds), the sensor uses aggressive parameters to transfer the 32-byte calibration data and 8-hour historical buffer quickly. It then relaxes to normal operation parameters and further reduces to background mode when the phone app enters the background.
Production pitfall discovered in field trials:
During the 2021 EU launch, 4.2% of Android users experienced “silent disconnections” where the phone reported a connected state but no glucose data arrived. Root cause: some Android BLE stacks do not enforce the supervision timeout correctly when the phone’s Doze mode suspends the BLE stack for 6+ minutes. Abbott’s fix was to implement an application-layer heartbeat: if no notification arrives within 180 seconds, the app forces a disconnect/reconnect cycle. This reduced silent disconnections from 4.2% to 0.08% of sessions.
Putting Numbers to It
How does peripheral latency enable months-long battery life?
The Abbott FreeStyle Libre 3 uses peripheral latency to dramatically reduce power consumption while maintaining a responsive connection. Let’s quantify the power savings:
Without peripheral latency (CI = 500 ms, latency = 0):
Radio wakes for every connection event: $ = = 2 $
Each event: 2 ms radio active at 10 mA, 498 ms sleep at 2 µA: $ = = = 41.99 $
Battery life with 35 mAh battery: $ = = 833 = 34.7 $
With peripheral latency = 4 (skip 4 of 5 events):
Effective wake interval: \(500 \times 5 = 2500\) ms (every 2.5 seconds): $ = = 0.4 $
Current calculation: $ = = = 10.0 $
Battery life: $ = = 3{,}500 = 145.8 $
Power savings ratio: $ = 4.2 $
This 4.2× improvement (from 35 to 146 days) demonstrates why peripheral latency is essential for battery-powered BLE devices. The sensor remains connected and responsive—when the phone app foregrounds, the peripheral can immediately stop skipping events—but saves 76% of power during normal operation.
7.10 GATT Design Trade-Offs: MTU Size, Notification Rate, and Throughput
Designing an efficient GATT service requires understanding how three interrelated parameters – MTU (Maximum Transmission Unit), notification rate, and connection interval – interact to determine real-world throughput and battery life. Making the wrong trade-off can cut throughput by 10x or halve battery life.
MTU and its impact on throughput:
The default BLE MTU is 23 bytes (20 bytes usable after ATT header). Modern BLE 4.2+ devices can negotiate MTU up to 517 bytes. Larger MTUs reduce per-byte overhead because each ATT transaction has fixed overhead regardless of payload size.
| MTU | Usable payload | ATT overhead | Overhead % | Effective throughput (1M PHY, 7.5ms CI) |
|---|---|---|---|---|
| 23 bytes | 20 bytes | 3 bytes | 15% | 21.3 kbps |
| 65 bytes | 62 bytes | 3 bytes | 5% | 66.1 kbps |
| 185 bytes | 182 bytes | 3 bytes | 2% | 194.1 kbps |
| 517 bytes | 514 bytes | 3 bytes | 0.6% | 296.4 kbps (link layer segmentation) |
At 185-byte MTU, a single connection event can transfer one full ATT notification per interval. Above 185 bytes, the link layer must segment the ATT PDU across multiple link layer packets within the same connection event, which requires the controller to support Data Length Extension (DLE). Not all BLE controllers support DLE – verify before assuming large MTUs will work.
Connection interval vs battery life:
The connection interval (CI) determines how often the central and peripheral exchange data. Shorter CI increases throughput but costs more power.
| Connection interval | Max notifications/sec | Throughput (20B payload) | Current draw (nRF52840) |
|---|---|---|---|
| 7.5 ms | 133 | 21.3 kbps | 3.8 mA average |
| 15 ms | 66 | 10.6 kbps | 2.1 mA average |
| 30 ms | 33 | 5.3 kbps | 1.2 mA average |
| 100 ms | 10 | 1.6 kbps | 0.42 mA average |
| 500 ms | 2 | 0.32 kbps | 0.09 mA average |
Worked example – wearable heart rate monitor:
A chest-strap heart rate monitor sends one 8-byte heart rate reading per second. What are the optimal GATT parameters?
Requirement: 1 reading/sec, 8 bytes, 12+ month battery life on CR2032 (225 mAh)
Option A: CI = 7.5 ms (maximum throughput)
Notifications per CI: 1 every 133 ms (1/sec needed, 132/sec wasted)
Average current: 3.8 mA
Battery life: 225 / 3.8 = 59 hours = 2.5 days
Result: UNACCEPTABLE (2.5 days vs 12 month target)
Option B: CI = 1000 ms (matched to data rate)
Notifications per CI: 1 per second (exact match)
Average current: 0.05 mA (50 uA)
Battery life: 225 / 0.05 = 4,500 hours = 187 days
Result: CLOSE but still short of 12 months
Option C: CI = 1000 ms + peripheral latency = 4
Peripheral skips 4 of every 5 connection events
Effective CI: 5000 ms (wakes every 5 seconds)
Buffers 5 readings, sends in single burst every 5 seconds
Average current: 0.018 mA (18 uA)
Battery life: 225 / 0.018 = 12,500 hours = 521 days = 17 months
Result: MEETS TARGET with 42% margin
Key insight: The connection interval should match the data generation rate, not the maximum throughput capability. A heart rate monitor sending 8 bytes per second needs CI = 1000 ms, not CI = 7.5 ms. Using peripheral latency to skip unnecessary connection events further reduces average current by 3–5x. Most BLE battery life problems stem from connection intervals that are too aggressive for the actual data rate.
Putting Numbers to It
Quantifying MTU’s impact on firmware update throughput:
A 256 KB firmware update transferred over BLE demonstrates why MTU negotiation matters. Compare default MTU (23 bytes) versus negotiated large MTU (185 bytes):
Option 1: Default MTU = 23 bytes
Usable payload after ATT 3-byte header: $ = 23 - 3 = 20 $
Total notifications needed for 256 KB transfer: $ = = = 13{,}107 $
With connection interval CI = 30 ms (33 events/sec), transfer time: $ = 13{,}107 = 393{,}210 = 6.55 $
Option 2: Negotiated MTU = 185 bytes
Usable payload: $ = 185 - 3 = 182 $
Total notifications: $ = = 1{,}440 $
Transfer time with same 30 ms interval: $ = 1{,}440 = 43{,}200 = 0.72 = 43 $
Speedup factor: $ = = 9.1 $
In production, this 9× speedup increased firmware update completion rate from 45% (users gave up during 6.5-minute wait) to 92% (43 seconds felt instantaneous). Always negotiate the largest MTU both devices support—it’s the difference between a usable update process and one users abandon.
7.11 Summary
This chapter covered the Bluetooth protocol stack and GATT architecture:
- Classic Bluetooth uses RFCOMM for stream-based data (audio, file transfer)
- BLE uses GATT for attribute-based data (sensors, configuration)
- GATT hierarchy: Profile -> Service -> Characteristic -> Descriptor
- Properties (Read, Write, Notify, Indicate) control data access patterns
- Notifications are fast but unconfirmed; Indications are confirmed but slower
- Standard services (Heart Rate, Battery, Device Info) ensure interoperability
- Custom services use 128-bit UUIDs for proprietary data
- Little-endian byte order is mandatory for BLE data formatting
Worked Example: MTU Negotiation Impact on Firmware Update Speed
Scenario: A BLE smart home sensor needs a firmware update (256 KB file). Compare transfer time with default MTU (23 bytes) versus negotiated large MTU (185 bytes).
Given:
- Firmware size: 256 KB = 262,144 bytes
- Connection interval: 30 ms (33 connection events/second)
- Packets per connection event: 1 (single notification per event)
- Link layer overhead: Assume 100% reliability (no retransmissions)
Option 1: Default MTU = 23 bytes
- ATT header: 3 bytes
- Usable payload per notification: 23 - 3 = 20 bytes
- Total notifications needed: 262,144 ÷ 20 = 13,107 notifications
- Connection events needed: 13,107 (one notification per event)
- Transfer time: 13,107 events × 30 ms = 393,210 ms = 6.55 minutes
Option 2: Negotiated MTU = 185 bytes
- ATT header: 3 bytes
- Usable payload: 185 - 3 = 182 bytes
- Total notifications: 262,144 ÷ 182 = 1,440 notifications
- Connection events: 1,440
- Transfer time: 1,440 × 30 ms = 43,200 ms = 0.72 minutes = 43 seconds
Speedup: 6.55 min ÷ 0.72 min = 9.1× faster with MTU negotiation
Why this matters in production:
A smart home company with 10,000 deployed sensors discovered a critical security bug requiring firmware update. With default MTU: - Update time per sensor: 6.55 minutes - User must stay near sensor (within BLE range) for entire duration - Completion rate: 45% (users gave up, moved away, or connection dropped)
With 185-byte MTU: - Update time: 43 seconds - User experience: “Update completes before user walks away” - Completion rate: 92%
Battery impact during update:
| MTU | Transfer time | Radio active | Average current (update) | Energy consumed |
|---|---|---|---|---|
| 23 bytes | 6.55 min | 100% | 10 mA | 10 mA × 6.55 min = 1.09 mAh |
| 185 bytes | 0.72 min | 100% | 10 mA | 10 mA × 0.72 min = 0.12 mAh |
Conclusion: Always negotiate the largest MTU both devices support. For firmware updates, MTU negotiation is not optional—it’s the difference between a usable update process and one users abandon.
Decision Framework: GATT Notify vs Read for Periodic Data
| Criterion | Client-initiated READ | Server-initiated NOTIFY | Winner |
|---|---|---|---|
| Update frequency | Client polls (e.g., every 5 sec) | Server pushes (when data changes) | Notify for high-rate |
| Power (peripheral) | RX listen + TX response each poll | TX only when data changes | Notify (sleep between updates) |
| Power (central) | TX poll + RX response | RX only when data available | Notify |
| Latency | Worst-case = poll interval | Instant (sub-millisecond after change) | Notify |
| Bandwidth | Wastes bandwidth on “no change” polls | Only sends when data changes | Notify |
| Control | Central controls rate | Peripheral controls rate | Context-dependent |
| Use case: temperature (changes slowly) | ✓ | Notify (push on 0.5°C change) | |
| Use case: on-demand query | ✓ | Read (user clicks “refresh”) |
Real-world decision:
Scenario: A BLE soil moisture sensor reports to a smartphone app. Soil moisture changes slowly (hours), but user may want instant readings when opening the app.
Hybrid approach:
- Enable Notify with characteristic property: NOTIFY
- Implement threshold-based push: Only notify when moisture changes by >5%
- Support Read for on-demand queries when app opens
Code example (simplified):
float lastMoisture = 0;
float currentMoisture = readSensor();
// Push notification only on significant change
if (abs(currentMoisture - lastMoisture) > 5.0) {
pChar->setValue(currentMoisture);
pChar->notify();
lastMoisture = currentMoisture;
}
// Also allow on-demand Read
pChar->setCallbacks(new MyCallbacks()); // onRead() refreshes valuePower analysis:
| Approach | Update frequency | Daily notifications | Energy (µAh/day) |
|---|---|---|---|
| Polling (Read every 60s) | Fixed 1440/day | N/A (client-initiated) | 1440 TX × 2 µAh = 2,880 µAh |
| Notify (threshold) | Variable (~10/day) | 10 | 10 TX × 2 µAh = 20 µAh |
Result: Notify with threshold-based push reduces daily energy by 144× compared to polling every minute.
When to use Read instead of Notify:
- User-initiated queries (settings pages, diagnostics)
- One-time configuration reads (device name, firmware version)
- Data that never changes (hardware revision, serial number)
Common Mistake: Assuming BLE Link Encryption Means End-to-End Security
The error: A developer building a smart door lock uses BLE pairing with “Just Works” mode and enables link-layer encryption. They assume the system is secure because “the data is encrypted.”
What actually happens:
BLE link-layer encryption (AES-CCM) protects confidentiality and integrity between the phone and the lock over the radio link. But it does NOT prevent a man-in-the-middle attack during the initial pairing.
Attack scenario (real exploit demonstrated at DEF CON 2019):
- Day 1: Legitimate user installs lock, pairs phone using “Just Works”
- Day 2: Attacker brings a fake “phone” device within range during a new pairing attempt (e.g., owner pairs a second phone)
- Lock advertises: “I’m a smart lock, ready to pair”
- Attacker’s device: Completes “Just Works” pairing before owner’s phone (no verification step)
- Attacker now has: Valid encryption keys and bonding (stored LTK)
- Owner’s second phone: Pairing fails (lock already paired)
- Later: Attacker returns, connects with stored keys, sends “unlock” command
The “encrypted” fallacy:
- Link-layer encryption protects after pairing
- It does NOT authenticate who you’re pairing with
- “Just Works” pairing has zero authentication bits
The fix (multiple layers required):
Use strong pairing method:
- Numeric Comparison (both devices show 6-digit code to verify)
- Out-of-Band (NFC tap, QR code scan)
- NOT “Just Works” for security-critical devices
Add application-layer authentication:
// Even after BLE pairing, require app-level challenge if (receivedCommand == "unlock") { if (!verifyUserToken(command.token)) { rejectCommand(); // Valid BLE connection, invalid user } }Implement authorization:
- BLE pairing = “you can talk to me”
- App authorization = “you have permission to unlock”
- These are separate security layers
Production example:
August Smart Lock (Gen 1) was vulnerable to MITM during pairing because it used “Just Works.” Fix required: - Firmware update to require Numeric Comparison (but lock had no display!) - Workaround: Required physical button press on lock during pairing (OOB-like) - App-layer: Required cloud account verification before accepting unlock commands
Key takeaway: Encryption protects data in transit. Authentication determines who gets the keys. Authorization determines what they can do with those keys. All three layers are necessary for security-critical IoT devices.
Common Pitfalls
1. Underestimating Stack Overhead in Throughput Calculations
Each BLE stack layer adds header bytes: LL (2B) + L2CAP (4B) + ATT (3B) = 9 bytes overhead per PDU. With default 23-byte ATT MTU, only 20 bytes carry application payload per packet. At 1 Mbps LE 1M PHY, theoretical max is ~125 packets/second × 20 bytes = 2.5 KB/s application throughput — far less than the 1 Mbps physical rate. Negotiate larger MTU (up to 517 bytes) for bulk transfers to approach practical maximums of ~96 KB/s.
2. Running BLE Host Stack and Application in Same Task Priority
BLE host stacks (NimBLE, Bluedroid) use internal tasks for protocol processing. Running application logic at the same or higher FreeRTOS priority than the BLE host task causes starvation, resulting in missed connection events and connection drops under application CPU load. Always run BLE host tasks at higher priority than application tasks, and use queues/semaphores for inter-task communication.
3. Using Legacy Pairing When LE Secure Connections is Available
BLE Legacy Pairing (pre-4.2) uses TK (Temporary Key) up to 6 decimal digits (Just Works = 000000), providing at most 20-bit entropy. An attacker passively recording the pairing exchange can brute-force the LTK offline in <1 second on modern hardware. Always use LE Secure Connections (LESC) which provides 128-bit security via ECDH, and verify peer LESC support before allowing pairing.
4. Ignoring L2CAP CoC for Large Data Transfers
Developers default to GATT for all BLE data transfer, including large file transfers. GATT over ATT has significant per-packet overhead and was not designed for bulk data. BLE 4.1+ supports L2CAP Connection-Oriented Channels (CoC) with configurable MPS up to 65535 bytes and credit-based flow control, achieving 2-4× better throughput than GATT for large payloads. Use L2CAP CoC for OTA firmware updates and large file transfers.
7.12 What’s Next
| Topic | Chapter | Why Read Next |
|---|---|---|
| Bluetooth Profiles (SPP, HID, A2DP) | Bluetooth Profiles | Apply the GATT and stack knowledge to major real-world profiles |
| BLE Connection Establishment | Bluetooth Connection Establishment | Understand how Link Layer advertising and scanning establish connections |
| Bluetooth Security & Pairing | Bluetooth Security | Secure GATT services with pairing, bonding, and link-layer encryption |
| BLE Classic vs BLE Comparison | Classic vs BLE | Extend the stack comparison to power, range, and use-case selection |
| BLE Implementation & Labs | Bluetooth Implementation | Write firmware using ESP32 BLE APIs to expose custom GATT services |
| Bluetooth Applications | Bluetooth Applications | See GATT services applied across healthcare, industrial, and consumer IoT |