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
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.
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);
}
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.
0x04 as the packet type indicator./*
* 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);
}
}
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.
0b10 = first fragment of a new L2CAP packet0b01 = continuing fragment0b11 = complete packet (no fragmentation)Also indicates flushable vs non-flushable
0b00 = point-to-point (normal data)0b01 = active broadcast to all slaves0b10 = 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.
PB=0b10 (START)
PB=0b01 (CONT)
PB=0b01 (CONT)
/* 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;
}
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.
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 correctlyHCI 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.
/* 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) |
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.
“I started it”
other work…
(device 1 found)
(device 2 found)
✅ Done
(takes seconds)
/* 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);
}
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.
Usually 1-2 deep (ncmd in Command Status).
e.g. MTU=1021 bytes, count=8 slots.
Read via HCI_LE_Read_Buffer_Size.
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"
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.
Fast CPU
Radio limited
Three Separate Flow Control Paths
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
/*
* 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 */
| 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.
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.
