Logo
  • Built-in search
  • Google search
Zephyr Project v: latest
Document Release Versions
latest
4.3.0
4.2.0
3.7.0 (LTS)
Downloads
PDF
zephyrproject.org Links
Project Home
SDK
Releases
  • Introduction
  • Developing with Zephyr
  • Kernel
  • OS Services
  • Build and Configuration Systems
  • Connectivity
    • Bluetooth
      • Supported features
      • Qualification
      • Stack Architecture
      • LE Host
      • LE Audio Stack
      • LE Audio resources
      • LE Controller
      • Application Development
      • API
        • Bluetooth Classic Host and profiles
        • Bluetooth LE Audio
        • Bluetooth LE Host
        • Bluetooth Mesh
        • Core Host and drivers
          • Logical Link Control and Adaptation Protocol (L2CAP)
          • Connection Management
          • Data Buffers
          • HCI Drivers
          • HCI RAW channel
          • Cryptography
        • Other
      • Tools
      • Shell
    • Controller Area Network (CAN) Bus Protocols
    • Networking
    • LoRa and LoRaWAN
    • USB
    • Modbus
  • Hardware Support
  • Contributing to Zephyr
  • Project and Governance
  • Security
  • Safety
  • Samples and Demos
  • Supported Boards and Shields
  • Releases

Reference

  • API
  • Kconfig Options
  • Devicetree Bindings
  • West Projects
  • Glossary
Zephyr Project
  • Docs / Latest »
  • Connectivity »
  • Bluetooth »
  • API »
  • Logical Link Control and Adaptation Protocol (L2CAP)
  • Open on GitHub Report an issue with this page

Logical Link Control and Adaptation Protocol (L2CAP)

L2CAP layer enables connection-oriented channels which can be enabled with the configuration option: CONFIG_BT_L2CAP_DYNAMIC_CHANNEL. These channels support segmentation and reassembly transparently, they also support credit based flow control making it suitable for data streams.

Channels instances are represented by the bt_l2cap_chan struct which contains the callbacks in the bt_l2cap_chan_ops struct to inform when the channel has been connected, disconnected or when the encryption has changed. In addition to that it also contains the recv callback which is called whenever an incoming data has been received. Data received this way can be marked as processed by returning 0 or using bt_l2cap_chan_recv_complete() API if processing is asynchronous.

Note

The recv callback is called directly from RX Thread thus it is not recommended to block for long periods of time.

For sending data the bt_l2cap_chan_send() API can be used noting that it may block if no credits are available, and resuming as soon as more credits are available.

Servers can be registered using bt_l2cap_server_register() API passing the bt_l2cap_server struct which informs what psm it should listen to, the required security level sec_level, and the callback accept which is called to authorize incoming connection requests and allocate channel instances.

#define PSM 0x29 /* example PSM */

static int l2cap_recv(struct bt_l2cap_chan *chan, struct net_buf *buf);
static void l2cap_connected(struct bt_l2cap_chan *chan);
static void l2cap_disconnected(struct bt_l2cap_chan *chan);

/* ops used for every channel instance */
static struct bt_l2cap_chan_ops l2cap_ops = {
	.recv       = l2cap_recv,
	.connected  = l2cap_connected,
	.disconnected = l2cap_disconnected,
};

/* Fixed array of channel instances, one per possible connection */
static struct bt_l2cap_chan l2cap_chans[CONFIG_BT_MAX_CONN];

static int accept_cb(struct bt_conn *conn, struct bt_l2cap_server *server,
		     struct bt_l2cap_chan **chan)
{
	uint8_t conn_index = bt_conn_index(conn);

	/* initialize the chosen entry */
	l2cap_chans[conn_index] = (struct bt_l2cap_chan){
		.ops = &l2cap_ops,
	};

	*chan = &l2cap_chans[conn_index];

	printk("L2CAP channel accepted, assigned chan[%d]\n", conn_index);

	return 0; /* accept */
}

static struct bt_l2cap_server server = {
	.psm = PSM,
	.sec_level = BT_SECURITY_L1,
	.accept = accept_cb,
};

A complete working example demonstrating L2CAP dynamic channels is available in the acceptor (server) sample: L2CAP Connection Oriented Channels (Acceptor)

Fixed Channels

The user can also define fixed channels using the BT_L2CAP_FIXED_CHANNEL_DEFINE macro. Fixed channels are initialized upon connection, and do not support segmentation. An example of how to define a fixed channel is shown below.

static struct bt_l2cap_chan fixed_chan[CONFIG_BT_MAX_CONN];

/* Callbacks are assumed to be defined prior. */
static struct bt_l2cap_chan_ops ops = {
    .recv = recv_cb,
    .sent = sent_cb,
    .connected = connected_cb,
    .disconnected = disconnected_cb,
};

static int l2cap_fixed_accept(struct bt_conn *conn, struct bt_l2cap_chan **chan)
{
    uint8_t conn_index = bt_conn_index(conn);

    *chan = &fixed_chan[conn_index];

    **chan = (struct bt_l2cap_chan){
        .ops = &ops,
    };

    return 0;
}

BT_L2CAP_FIXED_CHANNEL_DEFINE(fixed_channel) = {
    .cid = 0x0010,
    .accept = l2cap_fixed_accept,
};

Client Channels

Client channels can be initiated with use of bt_l2cap_chan_connect() API and can be disconnected with the bt_l2cap_chan_disconnect() API. Note that the latter can also disconnect channel instances created by servers.

See the initiator (client) sample for a complete example: L2CAP Connection Oriented Channels (Initiator)

API Reference

L2CAP

Related code samples

  • L2CAP Connection Oriented Channels (Acceptor)Accept incoming L2CAP Connection Oriented Channel connections.
  • L2CAP Connection Oriented Channels (Initiator)Initiate L2CAP Connection Oriented Channel connections and send data.

© Copyright 2015-2026 Zephyr Project members and individual contributors.

Last generated: Mar 11, 2026. Last source update: Jan 06, 2026.