HCI — Packet Formats, Commands & Flow Control

HCI — Packet Formats, Commands & Flow Control
How the Host and Controller talk to each other — byte by byte
4
Packet Formats
6
Command Groups
3
Flow Control Paths
btmon
Live Debug Tool

What We Cover in This Part

Hello students welcome to embeddedpathashalas free course on bluetooth development in c, this in lecture we will cover HCI — Packet Formats, Commands & Flow Control In the previous part we understood why HCI exists and what the four packet types are. Now we go one level deeper — we look at the exact byte layout of each packet, walk through all the HCI command groups, see how the controller reports events back to the host, and finally understand how flow control prevents the small controller from being overwhelmed by a fast host.

Key Terms in This Post

HCI — Packet Formats, Commands & Flow Control HCI Command Packet HCI Event Packet HCI ACL Data Packet HCI SCO Packet OpCode / OGF / OCF Event Code PB Flag BC Flag Connection Handle Command Complete Command Status LE Meta Event HCI Flow Control Controller Buffers NUM_COMPLETED_PACKETS

3.5.1.1 — HCI Command Packet Format

Every instruction the Host sends to the Controller is wrapped in an HCI Command Packet. The packet has a fixed header followed by command-specific parameters. Knowing this layout is essential when you are reading btmon output or writing raw HCI socket code.

HCI Command Packet — Byte Layout

Bit 0
Bit 8
Bit 16
Bit 31+

OpCode (16 bits)
OCF [9:0] | OGF [15:10]
Param Length
8 bits (total bytes)

Parameter 0
8 bits
Parameter 1
8 bits
… more parameters …
variable length

OpCode = OGF (6 bits) + OCF (10 bits)

Param Length = count of remaining bytes only

Parameters = command-specific data

The very first byte on the physical transport (USB/UART) is actually a packet type indicator (0x01 for Command) before the OpCode. BlueZ socket APIs hide this from you, but btmon shows the raw bytes.

/*
 * Dissecting an HCI_Create_Connection command in raw bytes:
 *
 *   01           — packet indicator (0x01 = Command)
 *   05 04        — OpCode 0x0405 (little-endian): OCF=0x005, OGF=0x01
 *   0D           — parameter length = 13 bytes
 *   AA BB CC     — BD_ADDR bytes 0-2
 *   DD EE FF     — BD_ADDR bytes 3-5
 *   18 CC        — Packet type bitmask
 *   01           — Page scan repetition mode R1
 *   00           — Page scan mode: Mandatory
 *   00 00        — Clock offset
 *   01           — Role switch: Allow slave
 */

/* Same command via BlueZ hci_lib — library builds the packet for you */
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

int create_connection(int dd, bdaddr_t *bdaddr)
{
    create_conn_cp cp;
    memset(&cp, 0, sizeof(cp));
    bacpy(&cp.bdaddr, bdaddr);
    cp.pkt_type       = htobs(HCI_DM1 | HCI_DM3 | HCI_DM5 |
                              HCI_DH1 | HCI_DH3 | HCI_DH5);
    cp.pscan_rep_mode = 0x02;   /* R2 */
    cp.clock_offset   = 0x0000;
    cp.role_switch    = 0x01;   /* allow slave */

    return hci_send_cmd(dd,
                        OGF_LINK_CTL,           /* 0x01 */
                        OCF_CREATE_CONN,        /* 0x0005 */
                        CREATE_CONN_CP_SIZE,
                        &cp);
}

3.5.1.2 — HCI Event Packet Format

The Controller sends Event Packets asynchronously to the Host. They arrive for two reasons: either as a reply to a command you sent, or as an unsolicited notification — like an incoming connection request or a disconnection from the remote side.

HCI Event Packet — Byte Layout

Bit 0
Bit 8
Bit 16
Bit 31+

Event Code
8 bits — identifies event type
Param Length
8 bits

Event Param 0
Event Param 1
… more params …
Physical transport prepends 0x04 as the packet type indicator.

✅ Command Complete Event (0x0E)
Sent after the controller finishes executing a command. Contains the return status + any return parameters (e.g., BD_ADDR after HCI_Read_BD_ADDR). This is synchronous — you wait for it.
⏳ Command Status Event (0x0F)
Sent immediately to say “I received your command and started working on it”. The task itself is not done yet. The Host is free to do other work. Another event arrives when the task completes (e.g., Connection Complete after HCI_Create_Connection).
/*
 * Example: Reading btmon output for Connection Complete Event
 *
 * Raw bytes on the wire (after 0x04 packet indicator):
 *   03           — Event Code: 0x03 = Connection Complete
 *   0B           — Param Length: 11 bytes
 *   00           — Status: 0x00 = Success
 *   01 00        — Connection Handle: 0x0001 (little-endian)
 *   AA BB CC DD  — BD_ADDR bytes 0-3
 *   EE FF        — BD_ADDR bytes 4-5
 *   01           — Link type: 0x01 = ACL
 *   00           — Encryption enabled: No
 */

/* Receiving events in BlueZ via HCI filter */
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <sys/socket.h>

void wait_for_connection_complete(int dd)
{
    struct hci_filter nf;
    hci_filter_clear(&nf);
    hci_filter_set_ptype(HCI_EVENT_PKT, &nf);
    hci_filter_set_event(EVT_CONN_COMPLETE, &nf);   /* 0x03 */
    setsockopt(dd, SOL_HCI, HCI_FILTER, &nf, sizeof(nf));

    unsigned char buf[HCI_MAX_EVENT_SIZE];
    int len = read(dd, buf, sizeof(buf));

    /* buf layout after hci_filter:
     *   buf[0] = HCI_EVENT_PKT (0x04)
     *   buf[1] = event code
     *   buf[2] = param length
     *   buf[3] = status
     *   buf[4..5] = connection handle (LE)
     *   buf[6..11] = BD_ADDR
     */
    evt_conn_complete *cc = (evt_conn_complete *)(buf + HCI_EVENT_HDR_SIZE + 1);

    if (cc->status == 0x00) {
        char addr[18];
        ba2str(&cc->bdaddr, addr);
        printf("Connected to %s, handle=0x%04x, encrypt=%d\n",
               addr, btohs(cc->handle), cc->encr_mode);
    } else {
        printf("Connection failed, status=0x%02x\n", cc->status);
    }
}

3.5.1.3 — HCI ACL Data Packet Format

Once a connection is up, all your actual data — L2CAP frames, file transfers, RFCOMM streams — travels inside HCI ACL Data Packets. The header carries a Connection Handle to route the data to the right link, plus two flag fields that handle fragmentation and broadcast.

HCI ACL Data Packet — Byte Layout

Bit 0
Bit 8
Bit 16
Bit 31+

Handle (12 bits)
Connection Handle
PB
2 bits
BC
2 bits

Data Total Length (16 bits)
Data payload (variable)

PB — Packet Boundary Flag
0b10 = first fragment of a new L2CAP packet
0b01 = continuing fragment
0b11 = complete packet (no fragmentation)
Also indicates flushable vs non-flushable
BC — Broadcast Flag
0b00 = point-to-point (normal data)
0b01 = active broadcast to all slaves
0b10 = piconet broadcast (parked slaves)

Fragmentation: One L2CAP Packet → Multiple ACL Packets

L2CAP sometimes builds frames larger than the controller’s buffer can take in one shot. The Host fragments it across multiple ACL packets using the PB flag to mark which is first and which are continuations.

L2CAP Fragmentation Over ACL

L2CAP Packet — 600 bytes (too large for one ACL buffer)
▼ Fragment

ACL #1
PB=0b10 (START)
bytes 0..251
ACL #2
PB=0b01 (CONT)
bytes 252..503
ACL #3
PB=0b01 (CONT)
bytes 504..599
▼ Reassemble
Original L2CAP Packet — reconstructed at the other end
/* Sending ACL data via BlueZ HCI socket (raw approach) */

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>

int send_acl_data(int dd, uint16_t handle,
                  const uint8_t *data, uint16_t data_len)
{
    /* Build ACL header: 4 bytes */
    struct {
        uint16_t handle_and_flags;  /* handle [11:0], PB [13:12], BC [15:14] */
        uint16_t dlen;
    } __attribute__((packed)) acl_hdr;

    /*
     * PB = 0b10 (0x2 shifted to bits 13:12) = first fragment / no fragmentation
     * BC = 0b00 = point-to-point
     * Combined: flags = 0x2000
     */
    acl_hdr.handle_and_flags = htobs((handle & 0x0FFF) | 0x2000);
    acl_hdr.dlen             = htobs(data_len);

    /* Allocate full packet buffer */
    uint8_t *pkt = malloc(1 + sizeof(acl_hdr) + data_len);

    pkt[0] = HCI_ACLDATA_PKT;  /* 0x02 — packet type indicator */
    memcpy(pkt + 1, &acl_hdr, sizeof(acl_hdr));
    memcpy(pkt + 1 + sizeof(acl_hdr), data, data_len);

    int ret = write(dd, pkt, 1 + sizeof(acl_hdr) + data_len);
    free(pkt);
    return ret;
}

3.5.1.4 — HCI Synchronous (SCO/eSCO) Data Packet

Voice call audio travels inside Synchronous Data Packets. The header structure is similar to ACL but the flag fields serve different purposes. Here the Packet Status flag tells the host whether the received audio was clean or had errors — important for deciding whether to conceal the glitch in audio playback.

HCI Synchronous Data Packet — Byte Layout

Bit 0
Bit 8
Bit 16
Bit 31+

Connection Handle (12 bits)
PS
2 bits
RES
2 bits

Data Total Length (16 bits)
Audio data payload (variable)

PS — Packet Status Flag values:
0x00 — Good packet, received correctly 0x01 — Invalid packet, data should be ignored 0x02 — No data received, slot was missed 0x03 — Partially valid, some bits received correctly
💡 SCO vs ACL: SCO packets are never retransmitted even if they arrive with errors — timing is more important than correctness for voice. The PS flag lets the audio codec decide how to conceal the error (e.g., repeat the last good sample). ACL packets, by contrast, are always retransmitted on error — reliability matters more than latency for data.

3.5.2 — HCI Commands and Events

HCI commands are grouped by OGF. Each group handles a specific area of the controller. Here is a practical tour of the groups you will encounter most often in real development.

OGF 0x01 — Link Control Commands
Control connection to other controllers
HCI_Inquiry HCI_Inquiry_Cancel HCI_Create_Connection HCI_Disconnect HCI_Remote_Name_Request

OGF 0x02 — Link Policy Commands
Control piconet topology and power modes
HCI_Hold_Mode HCI_Sniff_Mode HCI_Exit_Sniff_Mode HCI_Switch_Role HCI_Write_Link_Policy_Settings

OGF 0x03 — Controller and Baseband Commands
Configure the chip itself
HCI_Set_Event_Mask HCI_Reset HCI_Write_Local_Name HCI_Write_Class_of_Device HCI_Write_Scan_Enable

OGF 0x04 — Informational Parameters
Read-only info about the controller’s capabilities
HCI_Read_Local_Version_Information HCI_Read_BD_ADDR HCI_Read_Local_Supported_Features

OGF 0x05 — Status Parameters
Read the current state of a connection
HCI_Read_RSSI HCI_Read_Link_Quality HCI_Read_Transmit_Power_Level

OGF 0x08 — LE Controller Commands
BLE-specific commands — all go through this same OGF
HCI_LE_Set_Event_Mask HCI_LE_Read_Buffer_Size HCI_LE_Create_Connection HCI_LE_Set_Advertise_Enable HCI_LE_Set_Scan_Enable
/* Practical examples — common HCI commands via hciconfig and btmgmt */

# Read local BD_ADDR
$ hciconfig hci0 | grep "BD Address"
BD Address: AA:BB:CC:DD:EE:FF  ACL MTU: 1021:8  SCO MTU: 64:1

# Read firmware version info (HCI_Read_Local_Version_Information)
$ hciconfig hci0 version
hci0:   Type: Primary  Bus: USB
        BD Address: AA:BB:CC:DD:EE:FF  ACL MTU: 1021:8  SCO MTU: 64:1
        HCI Version: 5.0 (0x9)  Revision: 0x100
        LMP Version: 5.0 (0x9)  Subversion: 0x6119
        Manufacturer: Intel Corp. (2)

# Write local device name (HCI_Write_Local_Name)
$ hciconfig hci0 name "MyBTDevice"

# Enable page + inquiry scan (HCI_Write_Scan_Enable)
$ hciconfig hci0 piscan

# Read RSSI for a connected handle (via btmgmt or hcitool)
$ hcitool rssi AA:BB:CC:DD:EE:FF
RSSI return value: -42

# LE: Start advertising (HCI_LE_Set_Advertise_Enable)
$ btmgmt add-adv -d 02010604094D79446576 1
$ btmgmt advinfo

HCI Events — What the Controller Sends Back

Event Code Event Name When It Fires
0x01 Inquiry Complete Device scan / inquiry is done
0x02 Inquiry Result A nearby device was found during inquiry
0x03 Connection Complete ACL or SCO connection established
0x05 Disconnection Complete Connection terminated (local or remote)
0x0E Command Complete Command finished, here are the return params
0x0F Command Status Command received and started, not yet done
0x10 Hardware Error Controller hardware fault
0x13 Number of Completed Packets Controller freed N buffers — send more data now
0x3E LE Meta Event Wrapper for all LE-specific events (sub-code inside)

LE Meta Event (0x3E) — Sub-events
Sub 0x01
LE Connection Complete
Sub 0x02
LE Advertising Report
Sub 0x03
LE Connection Update Complete
Sub 0x04
LE Read Remote Features Complete
Sub 0x05
LE Long Term Key Request

Inquiry — Command Status vs Command Complete in Action

The Inquiry command is a perfect example of why both Command Status and Command Complete events exist. Inquiry can take several seconds while the controller scans all 32 frequencies. The host must not block waiting — it needs to stay responsive.

HCI Inquiry — Event Sequence

HOST
Send HCI_Inquiry ──▶
◀── Command_Status
“I started it”
Host free to do
other work…
◀── Inquiry_Result
(device 1 found)
◀── Inquiry_Result
(device 2 found)
◀── Inquiry_Complete
✅ Done

CONTROLLER
Receives command
Command_Status ──▶
Scanning 32 freq…
(takes seconds)
Inquiry_Result ──▶
Inquiry_Result ──▶
Inquiry_Complete ──▶
/* Performing device inquiry using BlueZ hci_lib */
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

void bt_inquiry(int dev_id)
{
    int dd = hci_open_dev(dev_id);

    /*
     * hci_inquiry internally sends HCI_Inquiry and collects
     * Inquiry_Result events until Inquiry_Complete arrives.
     * max_rsp: max number of devices to find
     * len:     inquiry duration = len × 1.28 seconds
     */
    int max_rsp = 20;
    int len     = 8;  /* 8 × 1.28s = ~10 seconds */
    inquiry_info *ii = NULL;

    int num_rsp = hci_inquiry(dev_id, len, max_rsp, NULL, &ii, IREQ_CACHE_FLUSH);

    printf("Found %d device(s)\n", num_rsp);
    for (int i = 0; i < num_rsp; i++) {
        char addr[18];
        ba2str(&(ii + i)->bdaddr, addr);
        printf("  [%d] %s  class=0x%06x\n",
               i + 1, addr,
               (ii+i)->dev_class[2] << 16 |
               (ii+i)->dev_class[1] << 8  |
               (ii+i)->dev_class[0]);
    }

    free(ii);
    hci_close_dev(dd);
}

3.5.3 — Controller Buffers

The controller chip has a small amount of RAM to hold incoming commands and data from the host before they get transmitted over the air. Understanding buffer limits is critical — if you ignore them you will either corrupt data or trigger flow control stalls.

Controller Buffer Architecture

Command Buffer
Shared for BR/EDR & LE commands.
Usually 1-2 deep (ncmd in Command Status).

ACL BR/EDR Buffer
Size: read via HCI_Read_Buffer_Size.
e.g. MTU=1021 bytes, count=8 slots.

ACL LE Buffer
May be separate or shared with BR/EDR.
Read via HCI_LE_Read_Buffer_Size.

SCO Buffer
For synchronous audio data.
Reported in HCI_Read_Buffer_Size.
/* Read controller buffer sizes to know your limits */
$ hciconfig hci0
hci0:   ...
        ACL MTU: 1021:8  SCO MTU: 64:1
        #              ↑           ↑
        # ACL: max 1021 bytes/pkt, 8 packets simultaneously
        # SCO:  max 64  bytes/pkt, 1 packet simultaneously

/* Same info via HCI command in code */
struct hci_dev_info di;
hci_devinfo(dev_id, &di);
printf("ACL MTU: %d, ACL max pkts: %d\n",
       di.acl_mtu, di.acl_pkts);
printf("SCO MTU: %d, SCO max pkts: %d\n",
       di.sco_mtu, di.sco_pkts);

/* For LE — separate command needed if controller has separate LE buffers */
/* HCI_LE_Read_Buffer_Size (OGF=0x08, OCF=0x0002) */
$ btmgmt info | grep -i "le mtu\|le max"

3.5.4 — HCI Flow Control

The controller chip is physically small and has far less RAM than the Host CPU. If the host floods it with ACL packets faster than the radio can transmit them, the controller buffers fill up and packets get dropped. HCI Flow Control is the mechanism that prevents this.

The Core Problem
Host
Gigabytes of RAM
Fast CPU
⚡⚡⚡
Controller
~8 KB RAM
Radio limited
= Buffer Overflow ⚠️

Three Separate Flow Control Paths

Path 1
Host → Controller: ACL Data
The host tracks how many free buffer slots the controller has. It never sends more packets than there are free slots. The controller tells the host when buffers are freed via Number of Completed Packets events.

Path 2
Controller → Host: ACL Data
Rarely a problem in practice — the host usually has plenty of memory and can always absorb data coming from the controller. Optional flow control exists but is almost never used.

Path 3
Host → Controller: Commands
The ncmd field in Command Status / Command Complete events tells the host how many more commands the controller can accept right now. If ncmd = 0, the host must wait before sending the next command.

How Packet-Based Flow Control Works

Flow Control — Credit System
Step 1
Host reads buffer size: controller has 8 free ACL slots.
Step 2
Host sends 8 ACL packets. Counter drops to 0 free slots. Host stops sending.
Step 3
Controller transmits packets over radio. Sends Number_of_Completed_Packets event: “3 slots freed on handle 0x0001”.
Step 4
Host updates counter: 3 free slots. Sends 3 more ACL packets. Cycle continues.
/*
 * Number_of_Completed_Packets event (0x13) raw bytes:
 *
 *   04           — packet indicator (Event)
 *   13           — Event Code: Number_of_Completed_Packets
 *   05           — Param length: 5 bytes
 *   01           — Num_Handles: 1 connection reported
 *   01 00        — Connection_Handle: 0x0001 (little-endian)
 *   03 00        — Num_Completed_Packets: 3 (little-endian)
 *
 * This means: handle 0x0001 finished transmitting 3 ACL packets,
 * those 3 buffer slots are now free, host may send 3 more packets.
 */

/* BlueZ kernel module tracks this automatically via hci_num_comp_pkts() */
/* For your own HCI implementation, the counter logic looks like this: */

typedef struct {
    uint16_t handle;
    uint16_t total_slots;   /* from HCI_Read_Buffer_Size */
    uint16_t pending;       /* packets sent but not yet completed */
} acl_flow_ctx_t;

void on_num_completed_packets(acl_flow_ctx_t *ctx,
                              uint16_t handle,
                              uint16_t completed)
{
    if (ctx->handle == handle) {
        ctx->pending -= completed;
        /* Now can send (total_slots - pending) more packets */
        uint16_t can_send = ctx->total_slots - ctx->pending;
        printf("Handle 0x%04x: %d slots now free\n", handle, can_send);
    }
}

void send_acl_if_allowed(acl_flow_ctx_t *ctx,
                         int dd, const uint8_t *data, uint16_t len)
{
    if (ctx->pending < ctx->total_slots) {
        /* Safe to send */
        send_acl_data(dd, ctx->handle, data, len);
        ctx->pending++;
    } else {
        /* Controller buffers full — queue it and wait for completed event */
        printf("Controller buffer full — waiting for space\n");
    }
}

Command Flow Control via ncmd

Every Command Complete and Command Status event includes an ncmd byte that says how many commands the controller can accept right now. A value of 0 means: pause, do not send another command until you receive an event with ncmd > 0.

/*
 * Command Status event raw bytes (e.g., after HCI_Inquiry):
 *
 *   04           — packet type indicator (Event)
 *   0F           — Event Code: Command Status
 *   04           — Param length: 4
 *   00           — Status: 0x00 = Success (command accepted)
 *   01           — ncmd: 1 more command can be sent right now
 *   04 04        — OpCode of the command this status is for
 *                  (0x0404 = HCI_Inquiry)
 */

/* In btmon: */
$ sudo btmon 2>&1 | grep "ncmd\|Command Status\|Command Complete"

> HCI Event: Command Status (0x0f)
        HCI_Inquiry (0x01|0x0001) ncmd 1
        Status: Success (0x00)
        /* ncmd=1 means: you can send 1 more command right now */

> HCI Event: Command Complete (0x0e)
        HCI_Reset (0x03|0x0003) ncmd 1
        Status: Success (0x00)
        /* ncmd=1: also tells you reset is done AND you can send next command */

Quick Reference — HCI Packet Header Sizes
Packet Type Indicator Header Fields Header Size
HCI Command 0x01 OpCode (2B) + Param Length (1B) 3 bytes
HCI Event 0x04 Event Code (1B) + Param Length (1B) 2 bytes
HCI ACL Data 0x02 Handle+PB+BC (2B) + Data Length (2B) 4 bytes
HCI SCO Data 0x03 Handle+PS+RES (2B) + Data Length (2B) 4 bytes

Summary

HCI is not just an API — it is a well-defined binary protocol with precise packet formats. Every byte has a purpose. Commands carry OpCodes structured into group and function fields. Events carry Event Codes and arrive asynchronously. ACL packets carry the Connection Handle that routes them to the right link, plus PB/BC flags that manage fragmentation and broadcast. SCO packets add a Packet Status flag so the audio layer knows whether to conceal reception errors.

Flow control is the silent mechanism that keeps a slow controller from being overwhelmed by a fast host. The host watches the Number_of_Completed_Packets event stream as its credit counter, and watches the ncmd field in event headers to know how many commands it can queue. BlueZ handles all of this automatically in the kernel — but knowing it exists is what helps you diagnose stalls and dropped data in production.

✅ Command Packet Format ✅ Event Packet Format ✅ ACL Data Packet & Fragmentation ✅ SCO Data Packet & PS Flag ✅ All HCI Command Groups ✅ Command Complete vs Command Status ✅ LE Meta Event ✅ Controller Buffers ✅ Packet-Based Flow Control ✅ ncmd Command Flow Control

Next Up: L2CAP — Logical Link Control and Adaptation

We move above HCI and into L2CAP — the layer that multiplexes multiple logical channels over a single ACL link, negotiates MTU sizes, and handles segmentation and reassembly of large payloads.

HCI — Packet Formats, Commands & Flow Control i hope you are clear with this topic we will see more about bluetooth development in upcomming lectures.

Leave a Reply

Your email address will not be published. Required fields are marked *