What This Post Covers
Hello students welcome to embeddedpathashalas free bluetooth development course in c, using bluez. The previous chapter covered the lower layers — Radio, Baseband, LMP, and HCI. This post goes one level up and explains the upper layers of the Bluetooth Classic protocol stack. These layers sit above HCI and do the heavier lifting: multiplexing data streams, emulating serial ports, discovering services, and enabling audio streaming.
This is part one of the upper layers series. It covers the protocol stack architecture, the difference between core and adopted protocols, profiles, and the most important upper layer — L2CAP in detail. More layers (RFCOMM, SDP, OBEX) will be covered in the next post.
Key Terms in This Post
The upper layers of the Bluetooth Classic stack sit above the HCI interface. They provide richer services — like multiplexing multiple data streams, emulating a serial cable, exchanging files, or streaming audio — that applications can directly use.
The protocols in this region fall into two groups:
Core Protocols vs Adopted Protocols
| 🔵 Core Protocols | 🟢 Adopted Protocols |
|
Written from scratch specifically for Bluetooth L2CAP — Data multiplexing SDP — Service Discovery AVDTP — Audio/Video streaming AVCTP — AV remote control
|
Taken from other standards and reused RFCOMM — From ETSI TS 07.10 (serial port) OBEX — From IrOBEX (file exchange)
|
Figure 1 — Bluetooth Classic Stack: Upper Layers Position
|
||||||||||||
| ← Profiles (dotted border) | ||||||||||||
| OBEX (Adopted) | ||||||||||||
| SDP | RFCOMM (Adopted) | AVDTP | AVCTP | |||||||||
| L2CAP (Core Protocol) | ||||||||||||
| Host Controller Interface (HCI) | ||||||||||||
| Link Manager Protocol (LMP) | ||||||||||||
| Baseband Controller | ||||||||||||
| Bluetooth Radio | ||||||||||||
| Core Protocol Adopted ProtocolProfile | ||||||||||||
A Profile is not a protocol — it is a specification that describes which protocols to use and which features of each protocol to enable in order to support a particular use case. For example, the File Transfer Profile (FTP) tells you to use OBEX on top of RFCOMM on top of L2CAP, with specific options set. Profiles make sure two devices from different manufacturers work together for the same task.
L2CAP stands for Logical Link Control and Adaptation Protocol. It sits directly above the Baseband layer (accessed through HCI) and is the first upper-layer protocol that every other upper layer passes through.
Think of L2CAP as a smart pipe manager. The Bluetooth radio gives you one physical ACL link between two devices. L2CAP lets multiple protocols share that single link at the same time — each gets its own logical channel. It also handles breaking large data packets into smaller Baseband-sized chunks (segmentation) and reassembling them at the other end.
What L2CAP Provides to Upper Layers
|
|
||||
|
|
A channel in L2CAP is a logical data path between two L2CAP entities on different devices. There are two types:
L2CAP Channel Types
| Connection-Oriented Channel | Connectionless Channel |
|
|
Connection-Oriented Channel Flow
Figure 2 — L2CAP Connection-Oriented Channel Lifecycle
| Device A (Initiator) | Device B (Responder) |
|
L2CAP_CONNECTION_REQUEST |
|
|
L2CAP_CONNECTION_RESPONSE |
|
|
L2CAP_CONFIGURATION_REQUEST (QoS, MTU) |
|
|
L2CAP_CONFIGURATION_RESPONSE |
|
| ✅ Channel open — data transfer begins (up to 64 KB per packet) | |
|
L2CAP_DISCONNECT_REQUEST |
|
|
L2CAP_DISCONNECT_RESPONSE |
|
| 🔴 Channel closed | |
| Request ResponseDisconnect | |
Every endpoint of an L2CAP channel is identified by a Channel Identifier (CID). A CID is a local number — it is assigned by the L2CAP layer on each device independently. So Device A might assign CID 0x0040 for a channel while Device B assigns CID 0x0041 for the same channel on its side. Both are valid.
CIDs in the range 0x0001–0x003F are reserved as fixed channels — they are always available once an ACL link exists and do not need to be set up separately. CIDs from 0x0040 onwards are dynamically assigned by L2CAP for new connection-oriented channels.
Table 1 — L2CAP CID Name Space
| CID | Description |
|---|---|
| 0x0000 | Null identifier — not allowed in any packet |
| 0x0001 | L2CAP Signaling Channel (BR/EDR) — used to send all signaling commands: Connection Req/Rsp · Configuration Req/Rsp · Disconnection Req/Rsp · Echo Req/Rsp |
| 0x0002 | Connectionless channel — two uses: ① Master broadcasts to all Slaves in the piconet (no ACK, no retransmit) ② Unicast from Master or Slave to a single device |
| 0x0003 | AMP Manager Protocol — used for Bluetooth 3.0 + HS operations (alternate MAC/PHY) |
| 0x0004 | Attribute Protocol (ATT) — used by BLE GATT for reading/writing attributes (covered in later posts) |
| 0x0005 | LE L2CAP Signaling Channel — same role as 0x0001 but for BLE connections |
| 0x0006 | Security Manager Protocol (SMP) — handles BLE pairing and key distribution (covered in later posts) |
| 0x0007–0x003E | Reserved for future use |
| 0x0040–0xFFFF | Dynamically allocated — assigned by L2CAP when a new connection-oriented channel is opened. Each device assigns its own CID independently. |
In BlueZ, L2CAP is exposed through standard Linux sockets with AF_BLUETOOTH address family and BTPROTO_L2CAP protocol. You work with it just like TCP sockets — create, bind, connect, send, receive, close.
One important concept: when opening a dynamic L2CAP channel you specify a PSM (Protocol/Service Multiplexer) rather than a port number. The PSM identifies which upper-layer protocol the channel is for (e.g., RFCOMM uses PSM 3, AVDTP uses PSM 25).
L2CAP Server — Listen for Incoming Channel
/*
* l2cap_server.c — Accept an incoming L2CAP connection
* Compile: gcc l2cap_server.c -o l2cap_server -lbluetooth
* Run: sudo ./l2cap_server
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>
#define L2CAP_PSM 0x1001 /* custom PSM — must be odd, >= 0x1001 */
#define BUFSIZE 1024
int main() {
struct sockaddr_l2 addr = {0};
char buf[BUFSIZE];
int server_sock, client_sock;
socklen_t opt = sizeof(addr);
/* 1. Create an L2CAP socket */
server_sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
/* 2. Bind to local adapter (BDADDR_ANY = first available) on our PSM */
addr.l2_family = AF_BLUETOOTH;
addr.l2_psm = htobs(L2CAP_PSM);
bacpy(&addr.l2_bdaddr, BDADDR_ANY);
bind(server_sock, (struct sockaddr *)&addr, sizeof(addr));
/* 3. Listen for incoming connections */
listen(server_sock, 1);
printf("Waiting for L2CAP connection on PSM 0x%04x ...\n", L2CAP_PSM);
/* 4. Accept the connection — this blocks until a client connects */
client_sock = accept(server_sock, (struct sockaddr *)&addr, &opt);
char remote[18];
ba2str(&addr.l2_bdaddr, remote);
printf("Connected from: %s\n", remote);
/* 5. Read data from the channel */
int bytes = read(client_sock, buf, BUFSIZE);
if (bytes > 0) {
buf[bytes] = '\0';
printf("Received: %s\n", buf);
}
close(client_sock);
close(server_sock);
return 0;
}
L2CAP Client — Connect and Send Data
/*
* l2cap_client.c — Connect to an L2CAP server and send data
* Compile: gcc l2cap_client.c -o l2cap_client -lbluetooth
* Run: sudo ./l2cap_client
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>
#define REMOTE_BDADDR "00:1A:7D:DA:71:11" /* replace with your device */
#define L2CAP_PSM 0x1001
int main() {
struct sockaddr_l2 addr = {0};
int sock;
/* 1. Create the L2CAP socket */
sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
/* 2. Fill in the remote device address and PSM */
addr.l2_family = AF_BLUETOOTH;
addr.l2_psm = htobs(L2CAP_PSM);
str2ba(REMOTE_BDADDR, &addr.l2_bdaddr);
/* 3. Connect — this sends L2CAP_CONNECTION_REQUEST under the hood */
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect failed");
return 1;
}
printf("Connected to %s\n", REMOTE_BDADDR);
/* 4. Send a message */
const char *msg = "Hello from L2CAP client";
write(sock, msg, strlen(msg));
printf("Sent: %s\n", msg);
close(sock);
return 0;
}
Run It — Step by Step
# Terminal 1 — On the server machine (the device receiving the connection) sudo hciconfig hci0 pscan # make it connectable gcc l2cap_server.c -o l2cap_server -lbluetooth sudo ./l2cap_server # Terminal 2 — On the client machine (the device initiating) gcc l2cap_client.c -o l2cap_client -lbluetooth sudo ./l2cap_client # Expected output on server: # Waiting for L2CAP connection on PSM 0x1001 ... # Connected from: 00:1A:7D:DA:71:11 # Received: Hello from L2CAP client
Check L2CAP Channel from Terminal
# List all open L2CAP connections on your adapter sudo btmgmt info # Or use l2ping to test L2CAP connectivity to a remote device sudo l2ping 00:1A:7D:DA:71:11 # Output: # Ping: 00:1A:7D:DA:71:11 from 00:11:22:33:44:55 (data size 44) ... # 0 bytes from 00:1A:7D:DA:71:11 id 200 time 12.34ms # 0 bytes from 00:1A:7D:DA:71:11 id 201 time 11.78ms
| Protocol | Type | Sits On | What It Does |
|---|---|---|---|
| L2CAP | Core | HCI / Baseband | Data multiplexing, segmentation, QoS |
| SDP | Core | L2CAP | Discover what services a remote device offers |
| RFCOMM | Adopted | L2CAP | Serial port emulation — up to 60 virtual COM ports |
| OBEX | Adopted | RFCOMM | Object/file exchange (files, contacts, calendar) |
| AVDTP | Core | L2CAP | Audio/video streaming transport (used by A2DP) |
| AVCTP | Core | L2CAP | AV remote control commands (used by AVRCP) |
✏️ RFCOMM, SDP, OBEX and profiles (A2DP, HFP, SPP, FTP) will each be covered in dedicated posts in this series.
Next Up: RFCOMM & SDP
The next post covers RFCOMM serial port emulation and SDP service discovery — with BlueZ examples for both. Part of the free Bluetooth course on EmbeddedPathashala.
