The Generic Attribute Profile (GATT) layer defines how Bluetooth Low Energy devices expose data to clients. For engineers integrating Bluetooth modules into products, the GATT service architecture directly determines application performance, power consumption, and interoperability. This article covers practical GATT design decisions for module-based products.
GATT Hierarchy
GATT organizes data in a four-level hierarchy:
| Level | Description | Example |
|---|---|---|
| Profile | Collection of services for a use case | Heart Rate Profile |
| Service | Group of related characteristics | Heart Rate Service (0x180D) |
| Characteristic | Named value with properties | Heart Rate Measurement (0x2A37) |
| Descriptor | Metadata about a characteristic | Client Characteristic Configuration (0x2902) |
A module typically implements one or more primary services. Secondary services are only meaningful when referenced by another service — in practice, most module designs avoid them.
Service Design
Primary Service Registration
Each service is identified by a 128-bit UUID. Two allocation strategies exist:
- 16-bit UUIDs (0x1800–0x26FF): Assigned by Bluetooth SIG for standardized services. Use these when your module implements a standard profile (e.g., Battery Service 0x180F).
- 128-bit custom UUIDs: For proprietary services. Generate a base UUID and increment the lower bytes for each service and characteristic.
A common base UUID pattern:
Base: 6E40XXXX-B5A3-F393-E0A9-E50E24DCCA9E Service: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E Char 1: 6E400002-B5A3-F393-E0A9-E50E24DCCA9E Char 2: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
This approach, used by Nordic’s UART service, simplifies UUID management across a product line.
Characteristic Properties
Each characteristic declares properties that control how clients interact with it:
| Property | Opcode | Direction | Use Case |
|---|---|---|---|
| Read | 0x0A | Client → Server | Configuration values |
| Write | 0x12 | Client → Server | Control commands |
| Write Without Response | 0x52 | Client → Server | High-frequency commands |
| Notify | — | Server → Client | Sensor data streaming |
| Indicate | — | Server → Client | Critical alerts |
| Broadcast | — | Server → All | Beacon-style advertising |
Notification vs Indication
Both push data from server to client, but with different reliability guarantees:
- Notify (ATT_HANDLE_VALUE_NOTIFICATION): No acknowledgment. Latency: ~3 ms per packet at 1 Mbps PHY. Throughput: up to 270 kbps with 23-byte MTU, ~800 kbps with 247-byte MTU.
- Indicate (ATT_HANDLE_VALUE_INDICATION): Requires ATT confirmation from client. Adds one round-trip (~6–10 ms). Use for data where delivery confirmation is mandatory.
For a module streaming sensor data at 10 Hz, notification is the clear choice — the next sample supersedes any lost packet. For a module reporting alarm events, indication ensures the client received the alert.
Descriptor Design
Every characteristic that supports Notify or Indicate must include a Client Characteristic Configuration Descriptor (CCCD, UUID 0x2902). Writing 0x0001 to the CCCD enables notification; 0x0002 enables indication; 0x0000 disables both.
Optional but recommended descriptors:
| Descriptor | UUID | Purpose |
|---|---|---|
| Characteristic User Description | 0x2901 | Human-readable name |
| Characteristic Presentation Format | 0x2904 | Unit, exponent, data type |
| Characteristic Extended Properties | 0x2900 | Reliable Write support |
The Presentation Format descriptor is particularly valuable for modules serving multiple sensor types — it lets a client application auto-detect whether a value represents temperature (0x07, units 0x272F = Celsius) or humidity (0x06, units 0x272F as percentage).
MTU and Throughput Optimization
The default ATT MTU is 23 bytes (20 bytes payload). Most modern phones support MTU 247 or higher. The module should request MTU exchange during connection:
// Pseudo-code for MTU negotiation ble_att_exchange_mtu_request(247); // After exchange, effective MTU = min(local, remote) // Payload per notification = MTU - 3 bytes ATT header
Throughput comparison at different MTU values (connection interval 30 ms, 1 Mbps PHY):
| MTU | Payload/Packet | Packets/Interval | Throughput |
|---|---|---|---|
| 23 | 20 B | 4 | 21 kbps |
| 185 | 182 B | 1 | 48 kbps |
| 247 | 244 B | 1 | 65 kbps |
For Data Length Extension (DLE) capable modules (Bluetooth 4.2+), enabling DLE alongside large MTU can push throughput beyond 800 kbps on a 1 Mbps PHY.
Practical Example: Multi-Sensor Service
Consider a module integrating temperature, humidity, and accelerometer sensors. A flat service design:
Service: 6E400001-... (Custom Sensor Service) ├── Char: 6E400002-... (Temperature, Read|Notify) │ └── CCCD: 0x2902 │ └── Presentation Format: 0x2904 (int16, 0.01°C, unit Celsius) ├── Char: 6E400003-... (Humidity, Read|Notify) │ └── CCCD: 0x2902 │ └── Presentation Format: 0x2904 (uint16, 0.01%, unit percentage) ├── Char: 6E400004-... (Acceleration XYZ, Notify) │ └── CCCD: 0x2902 └── Char: 6E400005-... (Sampling Rate, Read|Write)
The Sampling Rate characteristic allows the client to configure how frequently the module pushes notifications — writing 10 sets 10 Hz, writing 0 disables all notifications.
Power Impact
Each notification consumes ~1.2 ms of radio active time at 1 Mbps. At 10 Hz with 20-byte payloads:
- Radio active per interval: ~12 ms per 1000 ms = 1.2% duty cycle
- Estimated current: 0.012 × 8 mA (TX) + 0.988 × 5 µA (sleep) ≈ 100 µA average
- CR2032 battery life (220 mAh): ~2200 hours ≈ 91 days
Reducing to 1 Hz: average current drops to ~13 µA, extending battery life to ~1.9 years.
Common Design Pitfalls
- Over-segmented services: Splitting related data across multiple services forces the client to perform multiple service discovery operations. Group related characteristics in one service.
- Missing CCCD: A characteristic with Notify property but no CCCD will cause some stacks to reject the service. Always include CCCD for notifiable characteristics.
- UUID collisions: Using random 128-bit UUIDs without a systematic base pattern risks collisions across product variants. Adopt a base UUID with sequential lower bytes.
- Excessive indications: Using Indicate for high-frequency data (e.g., 50 Hz IMU) wastes bandwidth on confirmations. Reserve Indicate for low-frequency, high-reliability events.
- Ignoring Presentation Format: Without the 0x2904 descriptor, client applications must hard-code data interpretation, breaking interoperability with generic GATT browsers like nRF Connect.
Conclusion
GATT service architecture is the application-layer contract between a Bluetooth module and its host. Thoughtful service organization, appropriate characteristic property selection, and correct descriptor usage determine whether a module integrates cleanly into third-party ecosystems or becomes a debugging burden. The principles above — flat service design, notification for streaming data, systematic UUID allocation, and MTU optimization — form a reliable foundation for module-based product development.