Chapter 51: Bluetooth Classic Introduction and Architecture

Chapter Objectives

By the end of this chapter, you will be able to:

  • Define Bluetooth Classic and understand its primary use cases.
  • Describe core Bluetooth concepts such as piconets, scatternets, and master/slave roles.
  • Explain the fundamental layers of the Bluetooth Classic protocol stack, including LMP, L2CAP, SDP, and RFCOMM.
  • Identify common Bluetooth Classic profiles like SPP, A2DP, HFP, and AVRCP and their applications.
  • Understand key Bluetooth parameters and processes such as device addressing, inquiry, paging, pairing, and bonding.
  • Articulate the main differences between Bluetooth Classic and Bluetooth Low Energy (BLE).
  • Recognize which ESP32 variants support Bluetooth Classic and their specific capabilities.
  • Initialize the Bluetooth Classic stack on an ESP32 using ESP-IDF.
  • Perform a basic Bluetooth device discovery (inquiry).

Introduction

Bluetooth is a ubiquitous short-range wireless technology designed for exchanging data over small distances. While Bluetooth Low Energy (BLE) has gained prominence for low-power applications, Bluetooth Classic (often referred to as Bluetooth BR/EDR for Basic Rate/Enhanced Data Rate) remains highly relevant for scenarios requiring higher throughput, such as streaming audio or continuous data transfer. Many ESP32-based IoT products leverage Bluetooth Classic for applications like wireless speakers, data loggers, and interfaces to legacy Bluetooth devices.

In previous volumes, we explored WiFi connectivity. Now, we shift our focus to another critical wireless technology. This chapter serves as your introduction to the world of Bluetooth Classic on the ESP32. We will delve into its fundamental architecture, protocol stack, and core operational concepts. Understanding these foundations is essential before we tackle specific profile implementations like Serial Port Profile (SPP) or Advanced Audio Distribution Profile (A2DP) in subsequent chapters.

Theory

What is Bluetooth Classic?

Bluetooth Classic was originally conceived as a wireless replacement for cables (e.g., RS-232 serial cables). It operates in the 2.4 GHz Industrial, Scientific, and Medical (ISM) radio band, the same band used by WiFi and Bluetooth Low Energy. However, it employs a technique called Frequency Hopping Spread Spectrum (FHSS) to mitigate interference. In FHSS, the transmitter rapidly switches carrier frequencies (hops) across 79 channels (each 1 MHz wide) in a pseudo-random sequence known to both the transmitter and receiver. This makes the communication robust against interference on any single frequency.

Key characteristics of Bluetooth Classic include:

  • Range: Typically up to 10 meters (Class 2 devices), though Class 1 devices can reach up to 100 meters with higher power.
  • Data Rates:
    • Basic Rate (BR): Up to 1 Mbps (actual throughput around 721 kbps).
    • Enhanced Data Rate (EDR): Offers 2 Mbps (π/4-DQPSK) and 3 Mbps (8DPSK) modulation schemes, leading to higher practical throughput (up to ~2.1 Mbps).
  • Power Consumption: Generally higher than BLE, making it more suitable for devices that are not strictly battery-constrained for long periods or can handle recharging.
  • Connection-Oriented: Designed for establishing relatively stable connections for data streaming or serial communication.
Characteristic Description
Operating Frequency 2.4 GHz ISM (Industrial, Scientific, and Medical) band (2.402 GHz to 2.480 GHz).
Interference Mitigation Frequency Hopping Spread Spectrum (FHSS) across 79 channels (1 MHz wide).
Range Typically up to 10 meters (Class 2 devices). Class 1 devices can reach up to 100 meters with higher power.
Data Rates (Throughput) Basic Rate (BR): Up to 1 Mbps (actual throughput ~721 kbps).
Enhanced Data Rate (EDR): Offers 2 Mbps (π/4-DQPSK) and 3 Mbps (8DPSK) modulation, leading to practical throughput up to ~2.1 Mbps.
Power Consumption Generally higher than Bluetooth Low Energy (BLE). More suitable for devices that are not strictly battery-constrained for long periods or can handle recharging.
Connection Type Connection-oriented, designed for establishing relatively stable connections for data streaming or serial communication.

Core Concepts

Piconet

A piconet is the fundamental network unit in Bluetooth. It consists of one master device and up to seven active slave devices. The master device dictates the timing and hopping sequence for the piconet. All communication is between the master and a slave; direct slave-to-slave communication is not possible within a single piconet.

  • Master Role: Initiates connections and manages the piconet.
  • Slave Role: Responds to the master.
graph TD;
    %% Piconet Diagram
    %% Styles
    classDef master fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef slave fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;

    M1["<b>Master</b><br>Device M1"];
    S1["Slave 1"];
    S2["Slave 2"];
    S3["Slave 3"];
    S4["<i>(up to 7 active slaves)</i>"];

    M1 --> S1;
    M1 --> S2;
    M1 --> S3;
    M1 -.-> S4;

    class M1 master;
    class S1,S2,S3 slave;
    class S4 slave;

    subgraph Piconet
        M1
        S1
        S2
        S3
        S4
    end

Scatternet

Multiple independent piconets can be linked together to form a scatternet. A device can participate in multiple piconets by acting as a slave in one piconet and a master in another, or as a slave in multiple piconets. This allows for more complex network topologies. However, participation in multiple piconets is time-multiplexed, meaning the device rapidly switches its attention between them.

graph TD;
    %% Scatternet Diagram
    %% Styles
    classDef master fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef slave fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef bridge fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; %% Decision/Bridge Node Style

    subgraph Piconet_A ["Piconet A"]
        MA["<b>Master A</b>"];
        SA1["Slave A1"];
        SAB["Device X<br>(Slave in Piconet A,<br>Master in Piconet B)"];
    end

    subgraph Piconet_B ["Piconet B"]
        MB["<b>Master B</b><br>(Device X)"];
        SB1["Slave B1"];
        SB2["Slave B2"];
    end

    MA --> SA1;
    MA --> SAB;

    %% Link showing SAB is also MB
    SAB -.-> MB; 

    MB --> SB1;
    MB --> SB2;
    
    class MA,MB master;
    class SA1,SB1,SB2 slave;
    class SAB bridge;

    %% Explanatory note
    Note["Device X participates in both piconets,<br>acting as a slave in Piconet A and master in Piconet B."];

Device Discovery (Inquiry)

Before a connection can be established, devices need to find each other. This process is called inquiry.

  1. A device wanting to discover others (the inquirer) enters the inquiry state and broadcasts inquiry packets on dedicated inquiry hop frequencies.
  2. Devices that are discoverable (in the inquiry scan state) listen for these inquiry packets.
  3. Upon receiving an inquiry packet, a discoverable device responds with an FHS (Frequency Hop Synchronization) packet containing its Bluetooth Device Address (BD_ADDR), Class of Device (CoD), and other information.The inquirer collects these responses to build a list of nearby discoverable devices.
sequenceDiagram;
    %% Device Discovery (Inquiry) Process
    actor Inquirer;
    actor DiscoverableDevice as "Discoverable Device";

    %% Style definitions
    %% Default actor color is fine, using message colors for emphasis
    %% Colors for messages or activations could be:
    %% Primary/Start: #EDE9FE (bg), #5B21B6 (text/line)
    %% Process: #DBEAFE (bg), #2563EB (text/line)
    %% Response/Success: #D1FAE5 (bg), #059669 (text/line)

    participant Inquirer;
    participant DiscoverableDevice;

    Inquirer->>Inquirer: Enter Inquiry State;
    Note right of Inquirer: Wants to find other devices;
    
    loop Inquiry Hopping Sequence
        Inquirer-->>DiscoverableDevice: Broadcasts Inquiry Packet (on inquiry hop frequencies);
    end

    DiscoverableDevice->>DiscoverableDevice: Enter Inquiry Scan State;
    Note left of DiscoverableDevice: Listening for inquiry packets;

    DiscoverableDevice-->>Inquirer: Receives Inquiry Packet;
    
    DiscoverableDevice->>Inquirer: Responds with FHS Packet<br>(BD_ADDR, CoD, Clock Info, etc.);
    Note left of DiscoverableDevice: Provides its identity<br>and sync info.;

    Inquirer->>Inquirer: Collects FHS Packet(s);
    Note right of Inquirer: Builds list of nearby<br>discoverable devices.;
    
    Inquirer->>Inquirer: Inquiry Complete / Timeout;

Paging (Connection Setup)

Once a device has discovered another device and wishes to connect, it initiates the paging process.

  1. The initiating device (which will become the master) enters the page state and sends page packets specifically addressed to the target slave device using the slave’s BD_ADDR.
  2. The target device, if in the page scan state, listens for these page packets.
  3. Upon receiving a page packet, the target slave responds.
  4. A series of message exchanges follows to synchronize clocks and hopping patterns, establishing the piconet with the initiator as master and the target as slave.
sequenceDiagram
    %% Paging (Connection Setup) Process
    actor Initiator as "Initiator (Future Master)"
    actor TargetDevice as "Target Device (Future Slave)"

    Initiator->>Initiator: Has BD_ADDR of Target
    Initiator->>Initiator: Enter Page State
    Note right of Initiator: Wants to connect to a specific device

    loop Paging Attempts on Target's Hop Sequence
        Initiator-->>TargetDevice: Sends Page Packet (using Target's BD_ADDR)
    end

    TargetDevice->>TargetDevice: Enter Page Scan State
    Note left of TargetDevice: Listening for page packets addressed to it

    TargetDevice-->>Initiator: Receives Page Packet
    TargetDevice->>Initiator: Responds to Page Packet

    Initiator->>TargetDevice: Master sends FHS packet
    Note over Initiator, TargetDevice: To synchronize hopping sequence and timing
    TargetDevice->>Initiator: Slave confirms receipt (sends slave ID packet)
    
    rect rgb(255, 1285, 156)
        Note over Initiator, TargetDevice: Further message exchanges <br> for parameter negotiation <br>(e.g., LMP versions, features)
        Initiator<<->>TargetDevice: LMP Handshake (e.g., link configuration)
    end

    rect rgb(144, 238, 144)
        Note over Initiator, TargetDevice: Connection Established!
        Note over Initiator, TargetDevice: Initiator becomes Master, Target becomes Slave
        Note over Initiator, TargetDevice: Piconet formed
    end

Bluetooth Device Address (BD_ADDR)

Every Bluetooth device has a unique 48-bit (6-byte) address, similar to a MAC address in Ethernet or WiFi. This address is typically assigned during manufacturing. Example: DE:AD:BE:EF:CA:FE.

Class of Device (CoD)

The Class of Device is a 24-bit value that indicates the type of device (e.g., computer, phone, audio device) and the services it supports. This helps in filtering devices during discovery. For instance, a phone might only look for audio headsets.

Bluetooth Classic Protocol Stack

The Bluetooth Classic protocol stack defines how data is processed and communicated between devices. It’s a layered architecture.

graph TB;
    %% Bluetooth Classic Protocol Stack
    %% Styles
    classDef radio fill:#E0E7FF,stroke:#3730A3,stroke-width:1px,color:#3730A3;
    classDef baseband fill:#C7D2FE,stroke:#4338CA,stroke-width:1px,color:#4338CA;
    classDef lmp fill:#A5B4FC,stroke:#4F46E5,stroke-width:1px,color:#4F46E5;
    classDef hci fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; 
    classDef l2cap fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; 
    classDef service fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46; 
    classDef profile fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; 

    %% Core Stack Layers
    RADIO["<b>Radio</b><br>(Physical Layer - FHSS, GFSK, DPSK)"];
    BASEBAND["<b>Baseband</b><br>(Physical Channels, Links, Packets, Hopping Sequence)"];
    LMP["<b>Link Manager Protocol (LMP)</b><br>(Link Setup, Auth, Encryption, Power Control)"];
    HCI["<b>Host Controller Interface (HCI)</b><br>(Standardized Command Interface)"];
    L2CAP["<b>Logical Link Control and Adaptation Protocol (L2CAP)</b><br>(Multiplexing, Segmentation & Reassembly, QoS)"];

    %% Upper Layer Protocols & Services (above L2CAP)
    SDP["<b>Service Discovery Protocol (SDP)</b><br>(Discover services on other devices)"];
    RFCOMM["<b>RFCOMM</b><br>(Serial Port Emulation)"];
    TCS["TCS Binary<br>(Telephony Control)"];
    ADOPTED["Adopted Protocols<br>(OBEX, PPP, TCP/IP etc.)"];

    %% Applications / Profiles (using the stack)
    APP_PROFILES["<b>Application Profiles</b><br>(Vertical Slices using the stack below)"];
    SPP["SPP<br>(Serial Port)"];
    A2DP["A2DP<br>(Audio Streaming)"];
    HFP["HFP<br>(Hands-Free)"];
    AVRCP["AVRCP<br>(Remote Control)"];
    HID["HID<br>(Input Devices)"];
    GAP["<b>Generic Access Profile (GAP)</b><br>(Discovery, Connection, Security - Fundamental)"];


    %% Connections
    BASEBAND --> RADIO;
    LMP --> BASEBAND;
    HCI --> LMP; 
    L2CAP --> HCI;
    
    SDP --> L2CAP;
    RFCOMM --> L2CAP;
    TCS --> L2CAP;
    ADOPTED --> L2CAP;
    ADOPTED --> RFCOMM; 

    APP_PROFILES --> SDP;
    APP_PROFILES --> RFCOMM;
    APP_PROFILES --> L2CAP;
    APP_PROFILES --> GAP; 

    subgraph "Host (e.g., ESP32 App Core)"
        L2CAP
        SDP
        RFCOMM
        TCS
        ADOPTED
        APP_PROFILES
        subgraph "Profiles"
            direction LR
            SPP
            A2DP
            HFP
            AVRCP
            HID
        end
        GAP
    end

    subgraph "Controller (e.g., ESP32 BT Hardware/Firmware)"
        HCI
        LMP
        BASEBAND
        RADIO
    end
    
    %% Styling
    class RADIO radio;
    class BASEBAND baseband;
    class LMP lmp;
    class HCI hci;
    class L2CAP l2cap;
    class SDP,RFCOMM,TCS,ADOPTED service;
    class APP_PROFILES,SPP,A2DP,HFP,AVRCP,HID,GAP profile;
  • Physical Layer (Radio): This is the lowest layer, responsible for the actual transmission and reception of radio waves. It defines specifications like frequency band (2.402 GHz to 2.480 GHz), modulation (GFSK for BR, π/4-DQPSK and 8DPSK for EDR), and transmission power. FHSS is managed at this level.
  • Baseband Layer: This layer manages the physical channels and links. It handles packet formatting, timing, and the actual hopping sequence. It establishes connections (ACL for data, SCO for audio) and manages piconet operations.
  • Link Manager Protocol (LMP): LMP is responsible for link setup and ongoing link management between devices. This includes:
    • Authentication and encryption (pairing).
    • Power control.
    • Negotiating packet sizes.
    • Managing device roles (master/slave switch).LMP messages are exchanged directly between the Link Managers of the connected devices.
  • Host Controller Interface (HCI): HCI provides a standardized command interface between the Bluetooth Host (typically the main CPU running upper layers of the stack and applications) and the Bluetooth Controller (the Bluetooth module handling lower layers like Radio, Baseband, and LMP). On ESP32, the Bluetooth controller firmware runs on one of the CPU cores or a dedicated co-processor, while the host stack (Bluedroid) runs on the other core.
  • Logical Link Control and Adaptation Protocol (L2CAP): L2CAP acts as a transport protocol over the baseband links. It provides connection-oriented and connectionless data services to the upper layers. Key functions include:
    • Protocol Multiplexing: Allows multiple protocols (like SDP, RFCOMM) to share a single baseband link.
    • Segmentation and Reassembly (SAR): Breaks down large packets from upper layers into smaller packets suitable for baseband transmission and reassembles them at the receiving end.
    • Quality of Service (QoS): Manages data flow and can provide QoS parameters for connections.
  • Service Discovery Protocol (SDP): SDP allows a Bluetooth device to discover the services offered by other Bluetooth devices and their characteristics (e.g., which profiles are supported, what L2CAP PSMs or RFCOMM channels to use). A device running a service will have an SDP server, and a device looking for services will act as an SDP client.
  • RFCOMM (Radio Frequency Communication): This protocol emulates an RS-232 serial port over L2CAP. It provides a simple, reliable data stream for applications. The Serial Port Profile (SPP) is based directly on RFCOMM. It supports up to 60 simultaneous connections (channels).
  • Telephony Control Protocol Specification – Binary (TCS Binary): Used for call control signaling in telephony profiles like HFP (though HFP often uses AT commands over RFCOMM for this now).
  • Adopted Protocols: Higher-level protocols like OBEX (Object Exchange), PPP (Point-to-Point Protocol), and even TCP/IP can run over Bluetooth, typically using L2CAP or RFCOMM as their transport.

Profiles

Bluetooth profiles define how devices use the underlying Bluetooth stack to perform specific functions or applications. A profile is essentially a vertical slice through the protocol stack, specifying which protocols and procedures are used for a particular use case. Both connecting devices must support the same profile for it to work.

Generic Access Profile (GAP)

This is the most fundamental profile and forms the basis for all other profiles. GAP defines how devices:

  • Discover other Bluetooth devices (inquiry).
  • Establish connections (paging).
  • Manage security aspects like pairing and authentication.
  • Determine device roles (master/slave).
  • Advertise and discover services.Every Bluetooth device must support GAP.

Serial Port Profile (SPP)

Emulates a serial cable connection (RS-232) between two devices. It uses RFCOMM for transport. SPP is widely used for generic data exchange, such as between an ESP32 and a smartphone app, or between an ESP32 and a Bluetooth-enabled sensor or module.

Advanced Audio Distribution Profile (A2DP)

Designed for streaming high-quality stereo audio from a source to a sink.

  • A2DP Source (SRC): The device that streams the audio (e.g., smartphone, ESP32 playing music).
  • A2DP Sink (SNK): The device that receives and plays the audio (e.g., wireless headphones, Bluetooth speaker, ESP32 connected to an amplifier).A2DP typically uses the SBC (Subband Codec) codec, which is mandatory, but can also support optional codecs like AAC or aptX for better audio quality if both devices agree.

Audio/Video Remote Control Profile (AVRCP)

Provides a standard interface to control TVs, Hi-Fi equipment, or other A/V devices. When used with A2DP, it allows a device (e.g., headphones) to send playback commands (play, pause, stop, next track, volume control) to the audio source (e.g., smartphone).

  • AVRCP Controller (CT): Sends control commands (e.g., headphones with buttons).
  • AVRCP Target (TG): Receives and acts on commands (e.g., smartphone playing music).

Hands-Free Profile (HFP)

Enables hands-free telephony, typically between a mobile phone and a hands-free car kit or headset. It allows for voice calls, redialing, voice dialing, etc.

  • Audio Gateway (AG): The device that is the gateway for audio (e.g., mobile phone).
  • Hands-Free Unit (HF): The device providing the hands-free interface (e.g., car kit, headset).HFP uses SCO (Synchronous Connection-Oriented) links for audio and AT commands over RFCOMM for control.

Human Interface Device (HID)

Allows wireless connection of human input devices like keyboards, mice, joysticks. While common in BLE, Bluetooth Classic also supports HID.

Pairing, Bonding, and Security

  • Pairing: The process of establishing a trusted relationship between two Bluetooth devices by creating one or more shared secret keys (link keys). During pairing, devices typically exchange information and may require user interaction (e.g., entering a PIN code or confirming a numeric comparison).
    • Legacy Pairing: Used in older Bluetooth versions (pre-2.1). Susceptible to some attacks. Often involved fixed PINs like “0000” or “1234”.
    • Secure Simple Pairing (SSP): Introduced in Bluetooth v2.1+EDR. SSP enhances security and simplifies the pairing process. It uses various association models like Numeric Comparison, Just Works, Passkey Entry, and Out Of Band (OOB).
  • Bonding: After successful pairing, the generated link key and other device information can be stored by both devices. This is called bonding. When bonded devices reconnect, they can use the stored link key to quickly establish an encrypted connection without repeating the full pairing process.
  • Authentication: The process of verifying that a device is who it claims to be, typically using the shared link key established during pairing.
  • Encryption: Encrypting the data transmitted over the Bluetooth link using the shared link key to prevent eavesdropping.
  • Security Modes: Bluetooth defines several security modes that dictate the security requirements for connections:
    • Security Mode 1 (Non-secure): No security measures are enforced.
    • Security Mode 2 (Service-level enforced security): Security is initiated after L2CAP channel establishment. Allows for flexible security policies per service.
    • Security Mode 3 (Link-level enforced security): Security is initiated before link setup is complete. All connections are subject to security procedures.
    • Security Mode 4 (Service-level enforced security with SSP): Similar to Mode 2 but mandates SSP features. This is the common mode for modern Bluetooth Classic devices.

Bluetooth Classic vs. Bluetooth Low Energy (BLE)

While both are “Bluetooth,” Classic and BLE are fundamentally different technologies optimized for different use cases. Many modern chips, including some ESP32 variants, are dual-mode, meaning they support both.

Class of Device (CoD) – 24 bits Bit: 23 0 Service Class (11 bits) e.g., Networking, Audio, Telephony Bits 23-13 Major Device Class (5 bits) e.g., Computer, Phone, Audio Bits 12-8 Minor Device Class (6 bits) Specific type Bits 7-2 Bits 1-0 (Format Type) Provides information about the device type and services it offers. Used for filtering during discovery. Example: A smartphone might be CoD 0x5A020C (Telephony, Phone, Smartphone). (Bit fields are illustrative of general structure)

It’s crucial to choose the right Bluetooth technology for your application based on these differences.

ESP-IDF Bluetooth Architecture

The ESP-IDF provides a comprehensive Bluetooth stack, historically based on Bluedroid (from Android Open Source Project) for both Classic Bluetooth and BLE. For BLE, ESP-IDF also offers an alternative, more lightweight stack called NimBLE. For Bluetooth Classic, Bluedroid is the primary stack.

The ESP-IDF Bluetooth stack is typically divided into two main parts:

  1. Controller: This includes the lower layers of the Bluetooth stack (Physical Layer, Baseband, LMP) and the HCI layer (Physical HCI part). The controller firmware runs on one of the ESP32’s cores (often the PRO_CPU if the APP_CPU is used for the application, or a dedicated co-processor on some chip architectures).
  2. Host: This includes the upper layers of the Bluetooth stack (Logical HCI part, L2CAP, SDP, RFCOMM, GAP, and profiles). The host stack runs as part of the user application on one of the ESP32’s cores (typically the APP_CPU).

Key initialization steps in ESP-IDF usually involve:

  • Initializing the NVS (Non-Volatile Storage), as the Bluetooth stack uses it to store bonding information and other configuration.
  • Initializing the Bluetooth controller: esp_bt_controller_init().
  • Enabling the Bluetooth controller: esp_bt_controller_enable() with a specific mode (e.g., ESP_BT_MODE_CLASSIC_BT).
  • Initializing the Bluedroid host stack: esp_bluedroid_init().
  • Enabling the Bluedroid host stack: esp_bluedroid_enable().
  • Registering callbacks for various Bluetooth events (e.g., GAP events, profile-specific events).

Practical Examples

Let’s get our hands dirty by initializing the Bluetooth Classic stack on an ESP32 and performing a basic device discovery.

Example 1: Initializing Bluetooth Classic Stack and Performing Device Discovery

This example will demonstrate:

  1. Initializing NVS.
  2. Initializing and enabling the Bluetooth controller and the Bluedroid host stack for Classic Bluetooth.
  3. Setting a device name.
  4. Registering a GAP callback to handle discovery events.
  5. Starting device discovery.

1. Project Setup (VS Code with ESP-IDF Extension):

  1. Create a new ESP-IDF project (e.g., bt_classic_intro).
  2. Open the sdkconfig.defaults file (or create it if it doesn’t exist) and add/ensure these lines are present:CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=y CONFIG_BT_CLASSIC_ENABLED=y CONFIG_BT_SSP_ENABLED=y # Enable Secure Simple Pairing CONFIG_BT_BLE_SMP_EN=y # Though not strictly for classic, often enabled CONFIG_BT_CTRL_MODE_EFF=2 # ESP_BT_MODE_CLASSIC_BT # You can set the device name here or in code CONFIG_BT_BLUEDROID_NAME="ESP32_Classic_Demo"
    Alternatively, run idf.py menuconfig and navigate to:
    • Component config -> Bluetooth -> [*] Bluetooth (Enable)
    • Component config -> Bluetooth -> Bluedroid Options -> [*] Classic Bluetooth
    • Component config -> Bluetooth -> Bluedroid Options -> [*] Secure Simple Pairing
    • Component config -> Bluetooth -> Bluetooth controller -> Bluetooth controller mode (BR/EDR/BLE/DUALMODE) -> CLASSIC_BT (or DUAL MODE if you need BLE too)
    • You can also set the (ESP32_Classic_Demo) Default Bluedroid Host name under Bluedroid Options.

2. C Code Implementation (main/bt_classic_intro_main.c):

C
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_bt_api.h" // For GAP API (discovery, etc.)
#include "esp_bt_device.h"  // For esp_bt_dev_set_device_name

static const char *TAG = "BT_CLASSIC_INTRO";

// GAP callback function
static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param);

void app_main(void)
{
    esp_err_t ret;

    // Initialize NVS.
    // NVS is used to store PHY calibration data and bonding keys.
    ESP_LOGI(TAG, "Initializing NVS...");
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
    ESP_LOGI(TAG, "NVS initialized.");

    // Initialize Bluetooth Controller
    // This initializes the controller with default configuration.
    ESP_LOGI(TAG, "Initializing Bluetooth controller...");
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Initialize controller failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "Bluetooth controller initialized.");

    // Enable Bluetooth Controller in Classic Bluetooth mode
    ESP_LOGI(TAG, "Enabling Bluetooth controller in Classic BT mode...");
    ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Enable controller failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "Bluetooth controller enabled in Classic BT mode.");

    // Initialize Bluedroid Host Stack
    ESP_LOGI(TAG, "Initializing Bluedroid host stack...");
    ret = esp_bluedroid_init();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Initialize Bluedroid failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "Bluedroid host stack initialized.");

    // Enable Bluedroid Host Stack
    ESP_LOGI(TAG, "Enabling Bluedroid host stack...");
    ret = esp_bluedroid_enable();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "Bluedroid host stack enabled.");

    // Register GAP callback function
    ESP_LOGI(TAG, "Registering GAP callback...");
    ret = esp_bt_gap_register_callback(esp_bt_gap_cb);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "GAP callback register failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "GAP callback registered.");

    // Set device name (can also be set via menuconfig)
    const char *device_name = "MY_ESP32_SPEAKER"; // Example device name
    ESP_LOGI(TAG, "Setting device name to '%s'...", device_name);
    ret = esp_bt_dev_set_device_name(device_name);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Set device name failed: %s", esp_err_to_name(ret));
        // Continue anyway, default name from menuconfig might be used
    } else {
        ESP_LOGI(TAG, "Device name set successfully.");
    }
    
    // Set discoverability and connectability mode
    // ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE makes the ESP32 visible to other devices
    // and allows them to connect.
    ESP_LOGI(TAG, "Setting device to connectable and discoverable...");
    ret = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
     if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Set scan mode failed: %s", esp_err_to_name(ret));
    } else {
        ESP_LOGI(TAG, "Device set to connectable and discoverable.");
    }


    // Start device discovery (inquiry)
    // Duration is in units of 1.28 seconds. 10 * 1.28s = 12.8 seconds.
    // Max_responses: 0 means unlimited responses.
    ESP_LOGI(TAG, "Starting device discovery (inquiry) for 12.8 seconds...");
    esp_bt_gap_inquiry_duration_t inquiry_duration = 10; // 10 * 1.28s = 12.8 seconds
    ret = esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, inquiry_duration, 0);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Start discovery failed: %s", esp_err_to_name(ret));
    } else {
        ESP_LOGI(TAG, "Device discovery started.");
    }
}

// GAP callback implementation
static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
    switch (event) {
        case ESP_BT_GAP_DISC_RES_EVT: { // Device discovery result event
            ESP_LOGI(TAG, "Discovery Result:");
            ESP_LOGI(TAG, "  BD_ADDR: %02x:%02x:%02x:%02x:%02x:%02x",
                     param->disc_res.bda[0], param->disc_res.bda[1], param->disc_res.bda[2],
                     param->disc_res.bda[3], param->disc_res.bda[4], param->disc_res.bda[5]);

            char name[ESP_BT_GAP_MAX_BDNAME_LEN + 1] = {0};
            for (int i = 0; i < param->disc_res.num_prop; i++) {
                if (param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_BDNAME) {
                    strncpy(name, (char *)param->disc_res.prop[i].val, ESP_BT_GAP_MAX_BDNAME_LEN);
                    name[ESP_BT_GAP_MAX_BDNAME_LEN] = '\0'; // Ensure null termination
                    ESP_LOGI(TAG, "  Name: %s", name);
                } else if (param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_COD) {
                    uint32_t cod = *(uint32_t *)param->disc_res.prop[i].val;
                    ESP_LOGI(TAG, "  Class of Device (CoD): 0x%06x", cod);
                } else if (param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_RSSI) {
                    int8_t rssi = *(int8_t *)param->disc_res.prop[i].val;
                    ESP_LOGI(TAG, "  RSSI: %d dBm", rssi);
                }
            }
            break;
        }
        case ESP_BT_GAP_DISC_STATE_CHANGED_EVT: { // Discovery state change event
            if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) {
                ESP_LOGI(TAG, "Device discovery stopped.");
                // You could start another discovery here or proceed to other tasks
            } else if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STARTED) {
                ESP_LOGI(TAG, "Device discovery started (again or continued).");
            }
            break;
        }
        case ESP_BT_GAP_RMT_SRVCS_EVT: // Remote services event (after SDP query)
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVCS_EVT: status=%d, num_uuids=%d", 
                     param->rmt_srvcs.stat, param->rmt_srvcs.num_uuids);
            // Further processing for discovered services would go here
            break;
        case ESP_BT_GAP_RMT_SRVC_REC_EVT: // Remote service record event
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVC_REC_EVT: status=%d", param->rmt_srvc_rec.stat);
            // Further processing for discovered service records
            break;
        case ESP_BT_GAP_AUTH_CMPL_EVT: { // Authentication complete event
            if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
                ESP_LOGI(TAG, "Authentication success: %s", param->auth_cmpl.device_name);
                esp_log_buffer_hex(TAG, param->auth_cmpl.bda, ESP_BD_ADDR_LEN);
            } else {
                ESP_LOGE(TAG, "Authentication failed, status: %d", param->auth_cmpl.stat);
            }
            break;
        }
        case ESP_BT_GAP_PIN_REQ_EVT: { // Legacy Pairing PIN code request
            ESP_LOGI(TAG, "ESP_BT_GAP_PIN_REQ_EVT min_16_digit: %d", param->pin_req.min_16_digit);
            if (param->pin_req.min_16_digit) {
                ESP_LOGI(TAG, "Input pin code: 0000 0000 0000 0000");
                esp_bt_pin_code_t pin_code = {0}; // Example fixed PIN
                esp_bt_gap_pin_reply(param->pin_req.bda, true, 16, pin_code);
            } else {
                ESP_LOGI(TAG, "Input pin code: 1234");
                esp_bt_pin_code_t pin_code;
                pin_code[0] = '1';
                pin_code[1] = '2';
                pin_code[2] = '3';
                pin_code[3] = '4';
                esp_bt_gap_pin_reply(param->pin_req.bda, true, 4, pin_code);
            }
            break;
        }
        case ESP_BT_GAP_CFM_REQ_EVT: // SSP User Confirmation Request
            ESP_LOGI(TAG, "ESP_BT_GAP_CFM_REQ_EVT Please compare the numeric value: %d", param->cfm_req.num_val);
            // For simplicity, auto-confirm. In a real app, you'd display this to the user.
            esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
            break;
        case ESP_BT_GAP_KEY_NOTIF_EVT: // SSP Passkey Notification
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_NOTIF_EVT passkey:%d", param->key_notif.passkey);
            break;
        case ESP_BT_GAP_KEY_REQ_EVT: // SSP Passkey Request
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_REQ_EVT Please enter passkey!");
            // For simplicity, no key input. In a real app, user would input this.
            // esp_bt_gap_ssp_passkey_reply(param->key_req.bda, true, 000000); // Example: 0
            break;
        
        // Add other GAP events as needed for your application
        default: {
            ESP_LOGI(TAG, "Unhandled GAP Event: %d", event);
            break;
        }
    }
}

3. Build Instructions:

  1. Open a terminal in VS Code.
  2. Ensure your ESP-IDF environment is sourced.
  3. Clean the project (optional, good for fresh build): idf.py fullclean
  4. Build the project: idf.py build

4. Run/Flash/Observe:

  1. Connect your ESP32 board to your computer.
  2. Flash the firmware: idf.py -p (YOUR_SERIAL_PORT) flash (e.g., idf.py -p /dev/ttyUSB0 flash or idf.py -p COM3 flash).
  3. Open the serial monitor: idf.py -p (YOUR_SERIAL_PORT) monitor.
  4. Observe the Output:
    • You should see log messages indicating the initialization steps for NVS, Bluetooth controller, and Bluedroid.The device name will be set.The ESP32 will start discovering nearby Bluetooth Classic devices.For each discovered device, its BD_ADDR, Name (if available), Class of Device (CoD), and RSSI will be printed.After the inquiry duration (12.8 seconds in this example), discovery will stop.
    Example Serial Output:
Plaintext
I (308) BT_CLASSIC_INTRO: Initializing NVS...
I (328) BT_CLASSIC_INTRO: NVS initialized.
I (328) BT_CLASSIC_INTRO: Initializing Bluetooth controller...
I (378) BT_CLASSIC_INTRO: Bluetooth controller initialized.
I (378) BT_CLASSIC_INTRO: Enabling Bluetooth controller in Classic BT mode...
I (408) BT_CLASSIC_INTRO: Bluetooth controller enabled in Classic BT mode.
I (408) BT_CLASSIC_INTRO: Initializing Bluedroid host stack...
I (438) BT_CLASSIC_INTRO: Bluedroid host stack initialized.
I (438) BT_CLASSIC_INTRO: Enabling Bluedroid host stack...
I (498) BT_CLASSIC_INTRO: Bluedroid host stack enabled.
I (498) BT_CLASSIC_INTRO: Registering GAP callback...
I (498) BT_CLASSIC_INTRO: GAP callback registered.
I (498) BT_CLASSIC_INTRO: Setting device name to 'MY_ESP32_SPEAKER'...
I (508) BT_CLASSIC_INTRO: Device name set successfully.
I (508) BT_CLASSIC_INTRO: Setting device to connectable and discoverable...
I (518) BT_CLASSIC_INTRO: Device set to connectable and discoverable.
I (518) BT_CLASSIC_INTRO: Starting device discovery (inquiry) for 12.8 seconds...
I (528) BT_CLASSIC_INTRO: Device discovery started.
I (1838) BT_CLASSIC_INTRO: Discovery Result:
I (1838) BT_CLASSIC_INTRO:   BD_ADDR: a0:b1:c2:d3:e4:f5
I (1848) BT_CLASSIC_INTRO:   Name: My Phone
I (1848) BT_CLASSIC_INTRO:   Class of Device (CoD): 0x5a020c
I (1858) BT_CLASSIC_INTRO:   RSSI: -55 dBm
I (2338) BT_CLASSIC_INTRO: Discovery Result:
I (2338) BT_CLASSIC_INTRO:   BD_ADDR: 11:22:33:44:55:66
I (2348) BT_CLASSIC_INTRO:   Name: Old Bluetooth Headset
I (2348) BT_CLASSIC_INTRO:   Class of Device (CoD): 0x240404
I (2358) BT_CLASSIC_INTRO:   RSSI: -70 dBm
...
I (13328) BT_CLASSIC_INTRO: Device discovery stopped.

Tip: To see your ESP32 being discovered, use another device (like your smartphone or a computer with Bluetooth) to scan for Bluetooth devices while the ESP32 is in discoverable mode. It should appear with the name you set (e.g., “MY_ESP32_SPEAKER” or the name from menuconfig).

Variant Notes

Bluetooth Classic support varies across the ESP32 family:

ESP32 Variant Bluetooth Classic (BR/EDR) Support Bluetooth Low Energy (BLE) Support Key Notes
ESP32 (Original) Yes (Full) Yes (Full) Dual Mode (Classic + BLE). Most versatile for combined applications.
ESP32-S2 No No Focuses on WiFi, enhanced security, USB OTG. No Bluetooth capabilities.
ESP32-S3 Yes Yes (v5.0) Dual Mode. Supports Bluetooth Classic (BR/EDR) with some packet type limitations (e.g., BR on 1-slot, EDR on 2/3-DH1). Generally functional for common profiles.
ESP32-C2 No Yes RISC-V. BLE only. Optimized for low-power IoT.
ESP32-C3 No Yes RISC-V. BLE only. Optimized for low-power IoT.
ESP32-C6 No Yes (v5.3) RISC-V. Wi-Fi 6 + BT/BLE 5.3 + 802.15.4 (Thread/Zigbee). No Bluetooth Classic.
ESP32-H2 No Yes RISC-V. BLE + 802.15.4 (Thread/Zigbee). No Bluetooth Classic. Optimized for Thread/Zigbee devices.

In summary, for Bluetooth Classic development, you should primarily target the ESP32 (original) or the ESP32-S3. Always verify the specific capabilities of your chosen ESP32 chip model from its datasheet and the ESP-IDF documentation.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
NVS Not Initialized Bluetooth initialization functions like esp_bt_controller_init() or esp_bluedroid_init() return errors (e.g., ESP_ERR_NVS_NOT_INITIALIZED).
Device fails to store bonding keys or other BT configurations persistently.
Ensure nvs_flash_init() is called at the beginning of app_main().
Check its return value and handle potential errors like ESP_ERR_NVS_NO_FREE_PAGES or ESP_ERR_NVS_NEW_VERSION_FOUND by erasing and re-initializing NVS if necessary.
Bluetooth Not Enabled in menuconfig Compile errors if BT headers/APIs are guarded by Kconfig flags.
Runtime errors: esp_bluedroid_init() or related functions fail.
Log messages may indicate Bluedroid or Classic BT is not enabled.
Run idf.py menuconfig.
Navigate to Component config -> Bluetooth and ensure:
[*] Bluetooth (is enabled)
Component config -> Bluetooth -> Bluedroid Options -> [*] Classic Bluetooth (is enabled)
– Set Bluetooth controller mode appropriately (e.g., CLASSIC_BT or DUALMODE).
Incorrect Controller Mode Classic Bluetooth APIs fail (esp_bt_gap_start_discovery(), etc.).
Device not discoverable via Classic inquiry or cannot perform Classic BT operations.
Log errors related to invalid controller state.
Ensure esp_bt_controller_enable() is called with ESP_BT_MODE_CLASSIC_BT or ESP_BT_MODE_BTDM (Dual Mode).
Verify this matches the Bluetooth controller mode set in menuconfig.
Device Discovery Fails / No Devices Found ESP_BT_GAP_DISC_RES_EVT callback is never triggered, or very few/unexpected devices are found.
Discovery seems to complete too quickly without results.
1. Ensure target devices are in discoverable/pairing mode.
2. Increase inquiry duration in esp_bt_gap_start_discovery() (e.g., inquiry_duration = 15 for ~19s).
3. Check ESP32 antenna connection (if external).
4. Test in an area with less 2.4 GHz interference.
5. Ensure your ESP32 itself is not set to non-discoverable if you expect it to be found.
Task Stack Overflow in Callbacks Unexplained crashes, Guru Meditation errors, or stack overflow panics, often during intense Bluetooth activity or after receiving events. Keep Bluetooth callback functions (e.g., esp_bt_gap_cb) short and efficient.
Avoid lengthy operations, blocking calls, or large local variable allocations within callbacks.
Offload complex processing to a separate application task using FreeRTOS queues or event groups.
As a last resort, consider increasing Bluedroid Task Stack Size in menuconfig, but prioritize offloading.
Pairing/Bonding Issues PIN code requests not handled, SSP confirmation fails, or devices don’t reconnect automatically after initial pairing.
Authentication complete event (ESP_BT_GAP_AUTH_CMPL_EVT) shows failure status.
1. Ensure SSP is enabled (CONFIG_BT_SSP_ENABLED=y).
2. Implement handlers for all relevant GAP events: ESP_BT_GAP_PIN_REQ_EVT (for legacy), ESP_BT_GAP_CFM_REQ_EVT (for SSP numeric comparison), ESP_BT_GAP_KEY_NOTIF_EVT, ESP_BT_GAP_KEY_REQ_EVT.
3. For SSP “Just Works” or Numeric Comparison, reply appropriately (e.g., esp_bt_gap_ssp_confirm_reply(bda, true)).
4. Ensure NVS is working to store bonding keys.

Exercises

  1. Customize Device Name and Class of Device (CoD):
    • Modify the provided example code to set a different Bluetooth device name programmatically using esp_bt_dev_set_device_name().
    • Research how to set the Class of Device (CoD) for your ESP32. Use esp_bt_gap_set_cod() to classify your ESP32 as a specific type of device (e.g., a “Portable Audio” device or a “Computer”). Verify the change using another Bluetooth scanning tool that displays CoD.
    • Hint: CoD is a bitmask. You’ll need to find the correct bits for Major Service Class, Major Device Class, and Minor Device Class from Bluetooth SIG specifications or online CoD calculators.
  2. Persistent Discovery and Filter by Name:
    • Modify the discovery logic. Instead of discovering for a fixed duration and then stopping, make the ESP32 continuously discover devices. When discovery stops (e.g., after the inquiry_duration), immediately start it again. (Be mindful of power consumption for such continuous operation).
    • In the ESP_BT_GAP_DISC_RES_EVT handler, add logic to only print details of discovered devices whose names start with a specific prefix (e.g., “MyPhone” or “AudioDev”).
  3. Basic Connection Attempt (Conceptual):
    • After a device is discovered (e.g., your phone), try to initiate a connection to it using esp_bt_gap_start_bond() (to pair and connect) or esp_bt_gap_connect() (if already paired or for non-bonding connection, though bonding is recommended). You’ll need the BD_ADDR of the target device.
    • Handle the ESP_BT_GAP_AUTH_CMPL_EVT to see if pairing/connection was successful.
    • This exercise is more conceptual as a full connection requires handling more events and potentially SDP queries depending on what you want to do post-connection. Focus on initiating the bond/connection and observing the immediate outcome.

Summary

  • Bluetooth Classic (BR/EDR) is a robust short-range wireless technology optimized for streaming and continuous data transfer, operating in the 2.4 GHz ISM band using FHSS.
  • Core concepts include piconets (master/slaves), scatternets, device discovery (inquiry), and connection establishment (paging).
  • The Bluetooth Classic protocol stack is layered, with key components being Radio, Baseband, LMP, HCI, L2CAP, SDP, and RFCOMM.
  • Profiles (like GAP, SPP, A2DP, HFP, AVRCP) define how Bluetooth is used for specific applications.
  • Pairing establishes trust and shared keys, while bonding stores these keys for future reconnections. Secure Simple Pairing (SSP) enhances security.
  • Bluetooth Classic differs significantly from BLE in terms of power consumption, data rate, and use cases.
  • On ESP32, Bluetooth Classic is supported by the Bluedroid stack, part of ESP-IDF. Initialization involves setting up NVS, the BT controller, and the Bluedroid host.
  • The ESP32 (original) and ESP32-S3 are the primary variants supporting Bluetooth Classic. Other variants like ESP32-S2, C3, C6, H2 do not.
  • Successful Bluetooth development requires careful initialization, configuration via menuconfig, and proper handling of GAP and profile-specific events.

Further Reading

Leave a Comment

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

Scroll to Top