Bluetooth Microphone Control Profile (MICP) — Deep Dive

Bluetooth Microphone Control Profile (MICP) — Deep Dive
Understand how phones mute your headset microphone over BLE, from GATT all the way to BlueZ C code
v1.0
Spec Version
2
Profile Roles
2
GATT Services
BLE
Primary Transport

What is MICP and Why Should You Care?

Picture this: you are on a video call, wearing wireless earbuds. The meeting app on your phone taps the mute button. Instantly, your headset microphone goes silent. No audio packets. No ring tone leak. Just silence — on command.

That tap-to-mute magic across a BLE link is exactly what the Microphone Control Profile (MICP) standardises. Published by the Bluetooth SIG’s Generic Audio Working Group in February 2021, MICP defines exactly how a controller device (phone, laptop, tablet) discovers and manipulates the mute state — and optionally the gain — of a microphone device (headset, earbuds, conference speaker, hearing aid).

Before MICP existed, vendors built proprietary solutions. A Sony headset and an Apple iPhone used different private commands. MICP fixes that with an open, standardised GATT-based protocol that any compliant device can speak.

This tutorial walks through the entire spec in plain English, with architecture diagrams, packet-level breakdowns, security rules, and real BlueZ code so you can implement both sides yourself.

Topics Covered

MICP MICS AICS GATT Server GATT Client Mute Characteristic Audio Input Control Point Gain Setting Change_Counter BLE Security Mode 1 Level 2 BlueZ BLE Bonding GAP Peripheral LE Secure Connections

Chapter 1 — The Big Picture: Where Does MICP Live?

MICP is a GATT profile. That means it lives entirely on top of the Generic Attribute Protocol (GATT), which itself runs over ATT (Attribute Protocol) over BLE L2CAP. You do not need BR/EDR Classic Bluetooth to use MICP, though the spec does describe BR/EDR security requirements for dual-mode devices.

The profile depends on exactly one thing: GATT. Everything else — pairing, bonding, advertising — follows standard BLE GAP procedures.

Protocol stack showing where MICP sits:

MICP — Microphone Control Profile
MICS (Microphone Control Service)  |  AICS (Audio Input Control Service)
GATT — Generic Attribute Profile
ATT — Attribute Protocol
L2CAP  →  LL (Link Layer)  →  PHY

MICP itself has zero novel wire formats. It does not invent new PDUs. It simply defines which GATT services to instantiate, which characteristics to read/write, and under what conditions. This makes it relatively easy to implement once you understand GATT.

Chapter 2 — The Two Roles: Microphone Device vs Microphone Controller

MICP defines exactly two roles. Every device in a MICP session plays one of them.

Role GATT Role Real-World Example What it Hosts
Microphone Device GATT Server Wireless headset, earbuds, conference mic, hearing aid MICS service (mandatory) + AICS instances (optional)
Microphone Controller GATT Client Smartphone, laptop, tablet, video conferencing dongle Nothing — it reads/writes the server’s attributes

The key insight is simple: the headset exposes controls, and the phone operates those controls. The headset is like a panel of switches bolted to the wall. The phone is the hand that flips those switches.

Role relationship and data flow:

📱 Microphone Controller
GATT Client
Reads Mute State
Writes Mute / Gain
Subscribes to Notifications
Read Mute
Set Mute / Gain
Notifications
🎤 Microphone Device
GATT Server
MICS (Primary)
Mute Characteristic
AICS (Secondary, optional)
Audio Input State
Gain Setting Properties
Audio Input Control Point

Important rule: AICS is always a secondary service included inside MICS, not a standalone primary service. A device can have zero, one, or several AICS instances — one per physical microphone input is a common design. The phone discovers AICS via GATT’s “Find Included Services” sub-procedure.

Chapter 3 — Services in Detail: MICS and AICS

3.1 — Microphone Control Service (MICS)

MICS is the top-level service. It has exactly one mandatory characteristic: the Mute characteristic.

Characteristic Requirement Possible Values Operations
Mute M 0x00 = Not Muted
0x01 = Muted
0x02 = Disabled
Read, Write (Mute/Unmute), Notify

The “Disabled” value (0x02) is interesting — it means the device does not allow muting at this moment (for example, an active emergency call). A Microphone Controller that tries to write 0x01 when the current value is 0x02 will receive an ATT error.

The Mute characteristic controls the entire device with a single bit. Want per-microphone control? That is what AICS is for.

3.2 — Audio Input Control Service (AICS)

AICS adds fine-grained per-input control. Each instance represents one audio input path inside the device. A dual-microphone headset might have two AICS instances — one for the boom mic and one for the ambient noise mic.

Characteristic Requirement (Controller) What It Tells You
Audio Input State C.1 Current gain setting, mute state, gain mode, and a Change_Counter
Gain Setting Properties C.1 Unit step size, min gain, max gain (defines valid range for Set Gain)
Audio Input Type O Type of physical input (microphone, line-in, bluetooth, etc.)
Audio Input Status O Active or Inactive — is this input currently producing audio?
Audio Input Control Point O Write-only command register: Set Gain, Mute, Unmute, Set Gain Mode
Audio Input Description O Human-readable UTF-8 string label (e.g., “Boom Mic”, “Ambience”)

C.1 = Mandatory if you implement the Audio Input Control Point. You cannot issue control commands without first reading the current state (specifically the Change_Counter). More on this in Chapter 5.

Chapter 4 — Topology: How a Real Device Is Structured

A dual-microphone headset (two mics mixed to one output) is the canonical example from the spec. Here is how the GATT attribute tree looks:

🎤 Mic 1 Input
(e.g., Boom Mic)
AICS Instance 1
Audio Input State
Gain Properties
Control Point
Description
MICS
Microphone Control Service
(Primary Service)
Mute Characteristic
Includes AICS 1 & 2
as secondary services
separate input path
🎤 Mic 2 Input
(e.g., Ambience)
AICS Instance 2
Audio Input State
Gain Properties
Control Point
Description

The MICS service is the single primary service a controller discovers first. It acts as a container. The two AICS instances are referenced as included services inside MICS. Discovery of AICS always goes through MICS — you find MICS first, then call “Find Included Services” to get the AICS handles.

There are no concurrency restrictions. Multiple controllers can connect and interact with the same device simultaneously — the spec imposes no limits there.

Chapter 5 — GATT Sub-Procedures the Controller Must Support

When you implement the Microphone Controller side, you need certain GATT sub-procedures available. The spec breaks them into mandatory and conditional. Here is a clear breakdown:

GATT Sub-Procedure Requirement When Needed
Discover All Primary Services C.1 At least one of C.1 group is mandatory
Discover Primary Services by UUID C.1 Faster — search for MICS UUID directly
Find Included Services C.2 Mandatory if you support AICS procedures
Discover All Characteristics of a Service C.3 At least one of C.3 group is mandatory
Discover Characteristics by UUID C.3 More targeted, less ATT traffic
Discover All Characteristic Descriptors M Always — to find the CCCD for notifications
Notifications (subscribe via CCCD) M Always — for Mute state change alerts
Read Characteristic Value M Always — to poll Mute and Audio Input State
Write Characteristic Value M Always — to mute, unmute, set gain
Read Characteristic Descriptors M Always
Write Characteristic Descriptors M Always — to write CCCD and enable notifications

The CCCD (Client Characteristic Configuration Descriptor) is the BLE mechanism for turning on notifications. When a controller writes 0x0001 to the CCCD of the Mute characteristic, the device will automatically send ATT Notification packets whenever the mute state changes. This replaces polling.

Chapter 6 — MICS Procedures: Reading and Controlling the Global Mute

There are three MICS sub-procedures. Only “Read Mute” is mandatory. The other two are optional but nearly always implemented in practice.

6.1 — Configure Mute Notifications (Optional but Recommended)

The controller writes 0x0001 to the CCCD of the Mute characteristic. After this, any time the device’s mute state changes (user presses the mute button on the headset, for instance), the device sends an unsolicited ATT Notification to the controller with the new value. This is far more efficient than polling.

Notification subscription sequence:

Controller Headset (Device)
Write CCCD = 0x0001
ATT Write Request
ATT Write Response
Success (0x00)
[User presses mute button on headset]
ATT Notification
Mute = 0x01 (Muted)

6.2 — Read Mute (Mandatory)

The controller issues a standard ATT Read Request on the Mute characteristic handle. The device responds with the current value: 0x00 (Not Muted), 0x01 (Muted), or 0x02 (Disabled). This is always mandatory — the controller must be able to read the initial state on connection.

6.3 — Set Mute (Optional)

To mute the entire device, the controller writes 0x01 to the Mute characteristic. To unmute, it writes 0x00. If the current value is 0x02 (Disabled), the write will be rejected with an application error.

This is a device-wide mute. It affects all microphone inputs simultaneously. For per-input control, use the AICS Audio Input Control Point described next.

Chapter 7 — AICS Procedures: Per-Input Gain and Mute Control

AICS is more complex than MICS because it supports gain (volume) control in addition to muting. The most important design decision in AICS is the Change_Counter synchronisation mechanism. Understanding this is key to getting AICS right.

7.1 — The Change_Counter: Why It Exists

Imagine two phones connected to the same headset simultaneously. Phone A reads the gain as 5. Phone B also reads it as 5. Then Phone A sets the gain to 8. Meanwhile, Phone B — still thinking the gain is 5 — also tries to set it to 7. If Phone B’s command arrived after Phone A’s, it would silently overwrite a gain change that Phone B does not even know happened. The result is unpredictable.

The Change_Counter prevents this. Every time any field in Audio Input State changes, the device increments a one-byte counter. A controller must read this counter and include it in every control command. If the device’s counter has moved on since you read it, the command is rejected with error 0x80 (Invalid Change Counter). You must re-read the state and try again with the fresh counter value.

Change_Counter synchronisation flow (Set Gain example):

Controller Device (AICS)
Read Audio Input State
State = {gain:5, muted:0, mode:Manual, counter:3}
Write Control Point:
Opcode=SetGain, Counter=3, Gain=8
If device counter still = 3:
✓ Write Response: Success (counter now = 4)
If device counter already moved to 4 (another client changed something):
✗ ATT Error: 0x80 Invalid Change Counter — retry

7.2 — Audio Input Control Point Opcodes

Every write to the Audio Input Control Point follows a fixed format: Opcode + Change_Counter + (optional extra parameter). The table below shows each command:

Opcode Byte 1 Byte 2 Byte 3 Effect
Set Gain Setting 0x01 Change_Counter Gain_Setting Sets gain to specified level (Manual mode only)
Unmute 0x02 Change_Counter Sets this input’s Mute field to Not Muted
Mute 0x03 Change_Counter Sets this input’s Mute field to Muted
Set Manual Gain Mode 0x04 Change_Counter Switches gain control to Manual (not if Automatic Only or Manual Only)
Set Automatic Gain Mode 0x05 Change_Counter Switches gain to Automatic AGC (not if Automatic Only or Manual Only)

Gain Mode rules: Some microphones have hardware-fixed gain modes. If the Audio Input State reports Automatic Only or Manual Only, the controller must not attempt to change the gain mode — that opcode will be rejected. Set Gain Setting is also blocked when the gain mode is Automatic (the hardware is controlling gain itself).

7.3 — Gain Setting Properties

Before calling Set Gain Setting, the controller should read Gain Setting Properties. It returns three values: the unit step size (how much one unit represents in dB), the minimum gain setting value, and the maximum gain setting value. Your new Gain_Setting parameter must fall within [min, max]. Submitting a value outside this range will result in an ATT error.

Chapter 8 — BlueZ Implementation: Putting It All Together in C

BlueZ (the Linux Bluetooth stack) exposes a D-Bus GATT API that lets you register GATT services, characteristics, and descriptors from userspace. Below are practical code patterns for both the Device side (GATT server) and Controller side (GATT client).

8.1 — MICS GATT Server: Exposing the Mute Characteristic

This pattern uses the BlueZ GATT D-Bus API. The Mute characteristic needs Read, Write, and Notify properties, plus a CCCD (Client Characteristic Configuration Descriptor) for notifications.

/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * MICP – Microphone Control Profile
 * MICS GATT Server skeleton (BlueZ style)
 * Based on BlueZ src/shared/gatt-db.c patterns
 */

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include "src/shared/gatt-db.h"
#include "src/shared/att.h"

/* Bluetooth SIG assigned UUIDs */
#define MICS_UUID       0x184D   /* Microphone Control Service */
#define MUTE_CHAR_UUID  0x2BC3   /* Mute Characteristic */
#define CCCD_UUID       0x2902   /* Client Characteristic Configuration */

/* Mute state values as defined in MICS spec */
#define MUTE_NOT_MUTED  0x00
#define MUTE_MUTED      0x01
#define MUTE_DISABLED   0x02

struct mics_server {
    struct gatt_db        *db;
    struct gatt_db_attribute *mics_service;
    struct gatt_db_attribute *mute_characteristic;
    struct gatt_db_attribute *mute_cccd;
    uint8_t               mute_value;   /* current mute state */
    uint16_t              cccd_value;   /* 0x0001 = notifications enabled */
};

/*
 * Called when the Microphone Controller reads the Mute characteristic.
 * We respond immediately with the current mute_value.
 */
static void mute_read_cb(struct gatt_db_attribute *attrib,
                         unsigned int id,
                         uint16_t offset,
                         uint8_t opcode,
                         struct bt_att *att,
                         void *user_data)
{
    struct mics_server *server = user_data;
    uint8_t val = server->mute_value;

    gatt_db_attribute_read_result(attrib, id, 0, &val, sizeof(val));
}

/*
 * Called when the Microphone Controller writes to Mute.
 * Only 0x00 (unmute) and 0x01 (mute) are valid writes.
 * 0x02 (Disabled) cannot be written by the client — it is set by the server.
 * If mute_value == MUTE_DISABLED, reject the write.
 */
static void mute_write_cb(struct gatt_db_attribute *attrib,
                          unsigned int id,
                          uint16_t offset,
                          const uint8_t *value,
                          size_t len,
                          uint8_t opcode,
                          struct bt_att *att,
                          void *user_data)
{
    struct mics_server *server = user_data;

    if (len != 1 || (value[0] != MUTE_NOT_MUTED && value[0] != MUTE_MUTED)) {
        /* BT_ATT_ERROR_APPLICATION: invalid value */
        gatt_db_attribute_write_result(attrib, id, 0x80);
        return;
    }

    if (server->mute_value == MUTE_DISABLED) {
        /* Muting is currently disabled – reject */
        gatt_db_attribute_write_result(attrib, id, 0x80);
        return;
    }

    server->mute_value = value[0];

    /* Notify all subscribed controllers */
    if (server->cccd_value & 0x0001) {
        gatt_db_attribute_notify(server->mute_characteristic,
                                 &server->mute_value, 1, NULL);
    }

    gatt_db_attribute_write_result(attrib, id, 0);
}

/*
 * Build and register the MICS primary service in the GATT database.
 */
void mics_server_init(struct mics_server *server, struct gatt_db *db)
{
    bt_uuid_t uuid;

    server->db = db;
    server->mute_value = MUTE_NOT_MUTED;
    server->cccd_value = 0x0000;

    /* Register MICS as a primary service */
    bt_uuid16_create(&uuid, MICS_UUID);
    server->mics_service = gatt_db_add_service(db, &uuid, true, 8);

    /* Add Mute characteristic: Read + Write + Notify */
    bt_uuid16_create(&uuid, MUTE_CHAR_UUID);
    server->mute_characteristic = gatt_db_service_add_characteristic(
        server->mics_service,
        &uuid,
        BT_ATT_PERM_READ | BT_ATT_PERM_WRITE,
        GATT_CHR_PROP_READ | GATT_CHR_PROP_WRITE | GATT_CHR_PROP_NOTIFY,
        mute_read_cb,
        mute_write_cb,
        server
    );

    /* Add CCCD descriptor so controllers can subscribe to notifications */
    bt_uuid16_create(&uuid, CCCD_UUID);
    server->mute_cccd = gatt_db_service_add_descriptor(
        server->mics_service,
        &uuid,
        BT_ATT_PERM_READ | BT_ATT_PERM_WRITE,
        NULL, NULL, NULL
    );

    gatt_db_service_set_active(server->mics_service, true);
}

8.2 — AICS GATT Server: Exposing One Audio Input Instance

AICS is a secondary service. It is included inside the MICS service via an GATT Include Definition. Below shows how to add a single AICS instance and the Audio Input State + Control Point characteristics.

/* Audio Input State layout (5 bytes): */
/* [0] Gain_Setting  [1] Mute  [2] Gain_Mode  [3] Change_Counter */
/* Gain Mode values */
#define GAIN_MODE_MANUAL_ONLY    0x00
#define GAIN_MODE_AUTOMATIC_ONLY 0x01
#define GAIN_MODE_MANUAL         0x02
#define GAIN_MODE_AUTOMATIC      0x03

#define AICS_UUID               0x1843
#define AUDIO_INPUT_STATE_UUID  0x2B77
#define GAIN_SETTING_PROP_UUID  0x2B78
#define AUDIO_INPUT_TYPE_UUID   0x2B79
#define AUDIO_INPUT_STATUS_UUID 0x2B7A
#define AUDIO_INPUT_CTRL_UUID   0x2B7B  /* Audio Input Control Point */
#define AUDIO_INPUT_DESC_UUID   0x2B7C

/* AICS Control Point opcodes */
#define OPCODE_SET_GAIN          0x01
#define OPCODE_UNMUTE            0x02
#define OPCODE_MUTE              0x03
#define OPCODE_SET_MANUAL_MODE   0x04
#define OPCODE_SET_AUTO_MODE     0x05

/* Application error: counter mismatch */
#define ERR_INVALID_CHANGE_COUNTER 0x80

struct aics_instance {
    struct gatt_db_attribute *service;
    uint8_t  gain_setting;       /* current gain 0..max */
    uint8_t  mute;               /* 0=not muted, 1=muted */
    uint8_t  gain_mode;          /* see GAIN_MODE_* */
    uint8_t  change_counter;     /* increments on any state change */
    uint8_t  gain_unit;          /* 1 unit = gain_unit * 0.1 dB */
    uint8_t  gain_minimum;
    uint8_t  gain_maximum;
};

/*
 * Control Point write handler.
 * Validates Change_Counter then applies the requested operation.
 */
static void aics_ctrl_write_cb(struct gatt_db_attribute *attrib,
                               unsigned int id,
                               uint16_t offset,
                               const uint8_t *value,
                               size_t len,
                               uint8_t opcode_att,
                               struct bt_att *att,
                               void *user_data)
{
    struct aics_instance *aics = user_data;

    if (len < 2) {
        gatt_db_attribute_write_result(attrib, id, BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN);
        return;
    }

    uint8_t opcode  = value[0];
    uint8_t counter = value[1];

    /* Change_Counter check — reject stale commands */
    if (counter != aics->change_counter) {
        gatt_db_attribute_write_result(attrib, id, ERR_INVALID_CHANGE_COUNTER);
        return;
    }

    switch (opcode) {

    case OPCODE_SET_GAIN:
        if (len < 3) {
            gatt_db_attribute_write_result(attrib, id,
                BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN);
            return;
        }
        if (aics->gain_mode == GAIN_MODE_AUTOMATIC ||
            aics->gain_mode == GAIN_MODE_AUTOMATIC_ONLY) {
            /* Cannot set gain in automatic mode */
            gatt_db_attribute_write_result(attrib, id, 0x81);
            return;
        }
        {
            uint8_t new_gain = value[2];
            if (new_gain < aics->gain_minimum || new_gain > aics->gain_maximum) {
                gatt_db_attribute_write_result(attrib, id, 0x82); /* out of range */
                return;
            }
            aics->gain_setting = new_gain;
        }
        break;

    case OPCODE_UNMUTE:
        aics->mute = 0x00;
        break;

    case OPCODE_MUTE:
        aics->mute = 0x01;
        break;

    case OPCODE_SET_MANUAL_MODE:
        if (aics->gain_mode == GAIN_MODE_MANUAL_ONLY ||
            aics->gain_mode == GAIN_MODE_AUTOMATIC_ONLY) {
            gatt_db_attribute_write_result(attrib, id, 0x83); /* not permitted */
            return;
        }
        aics->gain_mode = GAIN_MODE_MANUAL;
        break;

    case OPCODE_SET_AUTO_MODE:
        if (aics->gain_mode == GAIN_MODE_MANUAL_ONLY ||
            aics->gain_mode == GAIN_MODE_AUTOMATIC_ONLY) {
            gatt_db_attribute_write_result(attrib, id, 0x83);
            return;
        }
        aics->gain_mode = GAIN_MODE_AUTOMATIC;
        break;

    default:
        gatt_db_attribute_write_result(attrib, id, 0x80);
        return;
    }

    /* Increment Change_Counter on every successful state mutation */
    aics->change_counter++;

    gatt_db_attribute_write_result(attrib, id, 0);

    /* TODO: send ATT Notification on Audio Input State characteristic */
}

8.3 — Controller Side: Service and Characteristic Discovery

On the controller side, you connect, discover MICS, then walk through GATT discovery to find included AICS instances. The pattern below uses BlueZ’s bt_gatt_client API.

/*
 * Microphone Controller – service discovery with bt_gatt_client
 * Based on BlueZ tools/btgatt-client.c patterns
 */

#include "src/shared/gatt-client.h"

#define MICS_UUID_STR   "0000184d-0000-1000-8000-00805f9b34fb"
#define MUTE_CHAR_STR   "00002bc3-0000-1000-8000-00805f9b34fb"

struct micp_controller {
    struct bt_gatt_client *client;
    uint16_t mute_value_handle;
    uint16_t mute_cccd_handle;
    uint16_t aics_ctrl_handle;     /* Audio Input Control Point handle */
};

/*
 * Callback fired when GATT service discovery completes.
 * Walk all discovered services looking for MICS.
 */
static void service_discovery_cb(bool success,
                                  uint8_t att_ecode,
                                  void *user_data)
{
    struct micp_controller *ctrl = user_data;

    if (!success) {
        fprintf(stderr, "GATT discovery failed: 0x%02x\n", att_ecode);
        return;
    }

    printf("GATT discovery complete – searching for MICS\n");

    /*
     * Iterate the GATT database snapshot and find MICS + its
     * Mute characteristic. In practice you would use
     * bt_gatt_client_get_services() iterator here.
     *
     * Pseudo-code for clarity:
     *
     *   for each service:
     *     if service.uuid == MICS_UUID:
     *       for each characteristic in service:
     *         if char.uuid == MUTE_CHAR_UUID:
     *           ctrl->mute_value_handle = char.value_handle
     *           ctrl->mute_cccd_handle  = char.ccc_handle
     *       // now find included AICS services
     *       for each included_service in service:
     *         if included_service.uuid == AICS_UUID:
     *           discover_aics_characteristics(included_service)
     */
}

/*
 * Subscribe to Mute notifications by writing 0x0001 to the CCCD.
 */
static void subscribe_mute_notifications(struct micp_controller *ctrl)
{
    uint8_t cccd_val[2] = { 0x01, 0x00 }; /* Little-endian 0x0001 */

    bt_gatt_client_write_value(
        ctrl->client,
        ctrl->mute_cccd_handle,
        cccd_val,
        sizeof(cccd_val),
        NULL,   /* write_callback – optional */
        NULL,
        NULL
    );

    printf("Subscribed to Mute notifications (CCCD handle 0x%04x)\n",
           ctrl->mute_cccd_handle);
}

/*
 * Mute the entire microphone device.
 */
static void set_device_mute(struct micp_controller *ctrl, bool muted)
{
    uint8_t val = muted ? 0x01 : 0x00;

    bt_gatt_client_write_value(
        ctrl->client,
        ctrl->mute_value_handle,
        &val,
        sizeof(val),
        NULL, NULL, NULL
    );

    printf("Wrote Mute = 0x%02x to handle 0x%04x\n",
           val, ctrl->mute_value_handle);
}

/*
 * Set gain on a specific AICS input.
 * Must supply the current Change_Counter value from Audio Input State.
 */
static void set_input_gain(struct micp_controller *ctrl,
                           uint8_t change_counter,
                           uint8_t new_gain)
{
    uint8_t payload[3];
    payload[0] = 0x01;           /* OPCODE_SET_GAIN */
    payload[1] = change_counter; /* MUST match server's current counter */
    payload[2] = new_gain;

    bt_gatt_client_write_value(
        ctrl->client,
        ctrl->aics_ctrl_handle,
        payload,
        sizeof(payload),
        NULL, NULL, NULL
    );

    printf("Set gain: counter=%u new_gain=%u\n", change_counter, new_gain);
}

Chapter 9 — Security: What You Must Configure

MICP takes security seriously. You cannot implement MICP with an unencrypted link. Here are the rules:

Requirement Controller Device
SM1 Level 1 (no security) ❌ Excluded ❌ Excluded
SM1 Level 2 (encrypted, unauthenticated) Optional C.2 – Min Required
SM1 Level 3 (encrypted, authenticated MITM) Optional C.2
SM1 Level 4 (LE Secure Connections, MITM) Optional C.2
128-bit Key Entropy C.1 (if L2 or L3) C.1 (if L2 or L3)
Bondable mode support ✅ Mandatory ✅ Mandatory

In practical terms: both sides must bond, and the link must be encrypted with at least a 128-bit key. The spec explicitly says Security Mode 1 Level 1 (plain unencrypted BLE) is excluded — you cannot connect and just start muting things without a pairing handshake.

The recommended key derivation path is LE Secure Connections. If the device is dual-mode (BR/EDR + LE), cross-transport key derivation from BR/EDR Secure Connections is also acceptable. Out-of-band (NFC tap, QR code scan) pairing is also valid.

Link Layer Privacy (resolvable private addresses) is recommended — this prevents passive tracking of the headset by scanning observers.

BlueZ: Requiring Encryption Before Characteristic Access

When registering GATT characteristics in BlueZ, pass the appropriate ATT permission flags to enforce encryption at the ATT layer:

/*
 * When adding the Mute characteristic, combine ENCRYPT_READ/WRITE
 * permissions to require an encrypted link before access is granted.
 * BlueZ will return ATT_ECODE_AUTHENTICATION if the link is unencrypted.
 */
server->mute_characteristic = gatt_db_service_add_characteristic(
    server->mics_service,
    &uuid,
    /* Permissions: require encrypted read + write */
    BT_ATT_PERM_READ  | BT_ATT_PERM_READ_ENCRYPT  |
    BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_ENCRYPT,
    GATT_CHR_PROP_READ | GATT_CHR_PROP_WRITE | GATT_CHR_PROP_NOTIFY,
    mute_read_cb,
    mute_write_cb,
    server
);
/* BT_ATT_PERM_READ_ENCRYPT  → error 0x0F if link not encrypted */
/* BT_ATT_PERM_WRITE_ENCRYPT → error 0x0F if link not encrypted */

Chapter 10 — GAP Requirements: Advertising, Discovery, and Connection

10.1 — Advertising (Microphone Device / Peripheral)

The headset (Peripheral) must use extended advertising PDUs — not legacy advertising — when trying to connect to a new device. The advertising data should include:

AD Data Type Required? Content
Flags Should LE General Discoverable Mode + BR/EDR Not Supported (or both supported)
Service UUID Should MICS UUID (0x184D) — allows the controller to filter by service UUID

Including the MICS UUID in advertising data is important for the controller. It lets a phone skip devices that do not support MICS without connecting and running full service discovery.

10.2 — Connection Interval Recommendation

The spec recommends a BLE connection interval of 10 to 30 milliseconds when acting as a Microphone Controller. This balances responsiveness (mute commands feel instant) against power consumption. A 100ms connection interval would make muting noticeably laggy.

/*
 * When requesting a connection to the headset, hint the preferred
 * connection parameters. The peripheral may reject or negotiate.
 *
 * Using hcitool or btmgmt:
 *   btmgmt conn-info <bdaddr>
 *
 * Or programmatically via BlueZ MGMT API after connection:
 */
struct mgmt_cp_set_conn_params params = {
    .addr       = peer_bdaddr,
    .min_interval = 8,    /* 8 * 1.25ms = 10ms */
    .max_interval = 24,   /* 24 * 1.25ms = 30ms */
    .latency      = 0,
    .timeout      = 100   /* 100 * 10ms = 1s supervision timeout */
};
/* Send via MGMT_OP_SET_CONN_PARAMS to kernel */

10.3 — Link Loss Reconnection

When the BLE link drops (moved out of range, interference), both sides should attempt to reconnect. The device re-enters its connectable advertising mode. The controller attempts reconnection using any of the standard GAP connection procedures — Auto Connection Establishment (using an allow list with the bonded device address) is most power-efficient.

Chapter 11 — Complete Session Flow: From Advertising to Muting

Let us trace a complete interaction from the moment you put on a MICP-capable headset to the moment the meeting app mutes your mic. Every numbered step corresponds to a real BLE operation.

📱 Phone (Microphone Controller) 🎧 Headset (Microphone Device)
① Scanning for BLE devices Advertising: Flags + MICS UUID (0x184D)
ADV_EXT_IND
② Sees MICS UUID → Connects + Pairs (SMP)
CONNECT_IND + SMP
Accepts connection, begins LE Secure Connections pairing
③ Discover All Primary Services
ATT Read By Group Type Req
Returns MICS handle range (e.g., 0x0010–0x001F)
④ Find Included Services (AICS)
ATT Find Information Req
Returns AICS instance handles
⑤ Discover Characteristics of MICS + AICS
Returns Mute, Audio Input State, Gain Props, AICP handles
⑥ Read Mute (handle 0x0011)
→ 0x00 (Not Muted)
⑦ Write CCCD = 0x0001 (enable notify)
OK
⑧ Meeting app taps Mute → Write Mute = 0x01
ATT Write Request
Mutes hardware mic, stores mute_value = 0x01
✅ Mute icon shown on screen
ATT Write Response
Success

Chapter 12 — Quick Reference Card
Item Value / Note
MICS UUID 0x184D
AICS UUID 0x1843
Mute Characteristic UUID 0x2BC3
Audio Input State UUID 0x2B77
Audio Input Control Point UUID 0x2B7B
Mute values 0x00 = Not Muted, 0x01 = Muted, 0x02 = Disabled
Invalid Change Counter error ATT App Error 0x80
Minimum security SM1 Level 2 (encrypted), 128-bit key, bonded
Recommended connection interval 10 – 30 ms
Bluetooth Core Spec requirement Any version that includes GATT (≥ BT 4.0)
Advertising PDU type (Device) Extended Advertising PDUs (not legacy)
CoD Major Service Class bit (BR/EDR) Bit 14 must be set to 1

Chapter 13 — Common Implementation Mistakes
❌ Forgetting the Change_Counter on AICS writes
Every write to the Audio Input Control Point must include the Change_Counter you last read from Audio Input State. Sending without it (or sending a stale value) will get error 0x80. The fix: always read Audio Input State immediately before each control write, or track the counter via notifications.
❌ Trying to Set Gain when Gain_Mode is Automatic
If the Audio Input State reports Gain_Mode = Automatic or Automatic Only, calling Set Gain Setting will be rejected. You must switch to Manual mode first (if the device permits it), then set the gain.
❌ Not using Find Included Services for AICS discovery
AICS is a secondary service — it will NOT appear in a “Discover All Primary Services” response. You must first find MICS, then call the “Find Included Services” GATT sub-procedure on the MICS handle range to discover AICS instances.
❌ Connecting without encryption
Accessing any MICS or AICS characteristic over an unencrypted link will result in an ATT error (Insufficient Encryption, 0x0F). Always pair and bond before reading or writing. Never skip SMP.
⚠️ Writing Mute when value is 0x02 (Disabled)
If Mute = 0x02 (Disabled), the device has locked muting. Attempting to write 0x01 will be rejected. Your application should read the mute state first and present the mute button as greyed out if the value is Disabled.
✅ Best Practice: Subscribe to notifications before polling
On connection, subscribe to Mute notifications and Audio Input State notifications first, then do a single read to get the initial state. After that, let notifications keep you up to date. This is far more power-efficient than periodic polling.

Keep Building with Embedded Pathashala

You now have everything you need to implement MICP on both the headset (GATT server) and phone (GATT client) sides — roles, services, characteristics, security, and working BlueZ C code patterns.

Next up: dive deeper into the Audio Input Control Service (AICS) specification, or explore how MICP fits alongside the Volume Control Profile (VCP) and Call Control Profile (CCP) in LE Audio headset architectures.

Browse All Courses BLE Protocol Series

Leave a Reply

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