【ESP32】ESP-IDF开发 | 经典蓝牙开发 | 协议栈基础(L2CAP、SDP、CAP) + 设备发现例程

1. 简介

        蓝牙技术是由爱立信(Ericsson)、诺基亚(Nokia)、东芝(Toshiba)、国际商用机器公司(IBM)和英特尔(Intel),于1998年5月联合宣布的一种无线通信新技术。蓝牙设备是蓝牙技术应用的主要载体,常见蓝牙设备比如电脑、手机等。蓝牙产品容纳蓝牙模块,支持蓝牙无线电连接与软件应用。蓝牙设备连接必须在一定范围内进行配对。这种配对搜索被称之为短程临时网络模式,也被称之为微微,可以容纳设备最多不超过8台。蓝牙设备连接成功,主设备只有一台,从设备可以多台。蓝牙技术具备射频特性。采用了TDMA结构网络多层次结构,在技术上应用了跳频技术、无线技术等,具有传输效率高、安全性高等优势,所以被各行各业所应用。

        蓝牙工作在全球通用的2.4GHz ISM(即工业、科学、医学)频段,使用IEEE802.15协议。蓝牙技术的主要工作范围在10米左右,传输带宽比较低,但优点是功耗极低。

        蓝牙协议栈分为经典蓝牙和低功耗蓝牙,后面的几篇主要以经典蓝牙为主。

1.1 硬件特性

        ESP32是唯一一个同时搭载经典蓝牙和低功耗蓝牙控制器的芯片,后面的型号都只搭载低功耗蓝牙控制器,它的主要特性如下:

  • 蓝牙 v4.2 完整标准,包含传统蓝牙 (BR/EDR) 和低功耗蓝牙 (Bluetooth LE)
  • 支持标准 Class-1、Class-2 和 Class-3,且无需外部功率放大器
  • 增强型功率控制 (Enhanced Power Control)
  • 输出功率高达 +9 dBm
  • NZIF 接收器具有–94 dBm 的 BLE 接收灵敏度
  • 自适应跳频 (AFH)
  • 基于 SDIO/SPI/UART 接口的标准 HCI
  • 高速 UART HCI,最高可达 4 Mbps
  • 支持蓝牙 4.2 BR/EDR 和 Bluetooth LE 双模 controller
  • 同步面向连接/扩展同步面向连接 (SCO/eSCO)
  • CVSD 和 SBC 音频编解码算法
  • 蓝牙微微网 (Piconet) 和散射网 (Scatternet)
  • 支持传统蓝牙和低功耗蓝牙的多设备连接
  • 支持同时广播和扫描

1.1 应用结构

        从整体结构上,蓝牙可分为控制器 (Controller) 和主机 (Host) 两大部分:控制器包括了 PHY、BasebandLink ControllerLink ManagerDevice ManagerHCI等模块,用于硬件接口管理、链路管理等等;主机则包括了L2CAPSMPSDPATTGATTGAP以及各种规范,构建了向应用层提供接口的基础,方便应用层对蓝牙系统的访问。

​        ESP-IDF在应用层支持2种操作库——Bluedroid和NimBLE。两者的区别在于,Bluedroid支持经典蓝牙和低功耗蓝牙,NimBLE仅支持低功耗蓝牙;但是NimBLE的资源消耗会更小,所以如果应用中只需要使用低功耗蓝牙,那么首选NimBLE。

1.2 经典蓝牙 

        ESP-IDF 中的经典蓝牙主机协议栈源于 BLUEDROID,后经过改良以配合嵌入式系统的应用。在底层中,蓝牙主机协议栈通过虚拟 HCI 接口,与蓝牙双模控制器进行通信;在上层中,蓝牙主机协议栈将为用户应用程序提供用于协议栈管理和规范的 API

        L2CAP 和 SDP 是经典蓝牙最小主机协议栈的必备组成部分,AVDTP、AV/C 和 AVCTP 并不属于核心规范,仅用于特定规范。

1.2.1 L2CAP协议

        L2CAP(蓝牙逻辑链路控制和适配协议)是 OSI 2 层协议,支持上层的协议复用、分段和重组及服务质量信息的传递。L2CAP 可以让不同的应用程序共享⼀条 ACL-U 逻辑链路。应用程序和服务协议可通过⼀个面向信道的接口,与 L2CAP 进行交互,从而与其他设备上的等效实体进行连接。

        L2CAP 信道共支持 6 种模式——基本 L2CAP 模式、流量控制模式、重传模式、加强重传模式、流模式、基于 LE Credit 的流量控制模式,可通过 L2CAP 信道配置过程进行选择,不同模式的应用场合不同,主要差别在于可提供的 QoS 不同。

1.2.2 SDP协议

        SDP(服务发现协议)允许应用程序发现其他对等蓝牙设备提供的服务,并确定可用服务的 特征。SDP 包含 SDP 服务器和 SDP 客户端之间的通信。服务器维护一个描述服务特性的服务记录表。客户端可通过发出 SDP 请求,从服务器维护的服务记录表中进行信息检索。

1.2.3 GAP协议

        GAP(通用访问规范)可提供有关设备可发现性、可连接性和安全性的模式和过程描述。目前,经典蓝牙主机协议栈仅提供少数几个 GAP API。应用程序可以将这些 API 用作“被动”设备,被对等蓝牙设备发现并连接。

2. 例程

        这个例程主要演示如何使用ESP-IDF的蓝牙API,初始化及启动相关服务,并且演示如何扫描周边设备并查询设备的可提供服务。

2.1 menuconfig配置

        ESP-IDF是默认没有使能蓝牙协议栈的,所以需要在menuconfig中配置。在VScode左侧的PlatformIO插件中,找到"Run Menuconfig"选项。

         进入Bluetooth目录,先配置主机使用Bluedroid。

        然后配置使能蓝牙控制器。

         接着配置Bluedroid,主要使能经典蓝牙、L2CAP协议、SDP协议

        最后配置控制器仅使用经典蓝牙协议栈。

         配置完按“S”保存配置,再按“Q”退出menuconfig。

2.2 代码

#include <stdint.h>
#include <string.h>
#include <inttypes.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"

#define TAG "app"

#define MAX_DEV_NUM 8
#define MAX_UUID_NUM 32

#define BDASTR "%02X:%02X:%02X:%02X:%02X:%02X"
#define BDA2STR(x) (x)[0], (x)[1], (x)[2], (x)[3], (x)[4], (x)[5]

typedef struct {
    esp_bt_uuid_t uuid[MAX_UUID_NUM];
    int len;
} uuid_list_t;

typedef struct {
    esp_bd_addr_t bda;
    uint32_t cod;
    int8_t rssi;
    char name[ESP_BT_GAP_MAX_BDNAME_LEN];
    uuid_list_t uuids;
} bt_dev_t;

typedef struct {
    bt_dev_t dev[MAX_DEV_NUM];
    int len;
} bt_dev_list_t;

static bt_dev_list_t devs;
static EventGroupHandle_t event_group = NULL;


static char *uuid2str(esp_bt_uuid_t *uuid, char *str, size_t size)
{
    if (uuid == NULL || str == NULL) {
        return NULL;
    }

    if (uuid->len == 2 && size >= 5) {
        sprintf(str, "%04x", uuid->uuid.uuid16);
    } else if (uuid->len == 4 && size >= 9) {
        sprintf(str, "%08"PRIx32, uuid->uuid.uuid32);
    } else if (uuid->len == 16 && size >= 37) {
        uint8_t *p = uuid->uuid.uuid128;
        sprintf(str, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
                p[15], p[14], p[13], p[12], p[11], p[10], p[9], p[8],
                p[7], p[6], p[5], p[4], p[3], p[2], p[1], p[0]);
    } else {
        return NULL;
    }

    return str;
}

static void print_device_info()
{
    for (int i = 0; i < devs.len; i++) {
        ESP_LOGI(TAG, "Device: " BDASTR, BDA2STR(devs.dev[i].bda));
        ESP_LOGI(TAG, "-- Class of Device: 0x%" PRIx32, devs.dev[i].cod);
        ESP_LOGI(TAG, "-- RSSI: %d", devs.dev[i].rssi);
        ESP_LOGI(TAG, "-- Name: %s", devs.dev[i].name);
        ESP_LOGI(TAG, "-- UUID:");
        for (int j = 0; j < devs.dev[i].uuids.len; j++) {
            char uuid_str[64] = {0};
            printf("%s, ", uuid2str(devs.dev[i].uuids.uuid, uuid_str, 64));
        }
        printf("\r\n");
    }
}

static void update_device_info(esp_bt_gap_cb_param_t *param)
{
    memcpy(devs.dev[devs.len].bda, param->disc_res.bda, sizeof(esp_bd_addr_t));

    esp_bt_gap_dev_prop_t *p = param->disc_res.prop;
    for (int i = 0; i < param->disc_res.num_prop; i++) {
        switch (p[i].type) {
            /* Class */
            case ESP_BT_GAP_DEV_PROP_COD:
            {
                uint32_t cod = *(uint32_t *)(p[i].val);
                devs.dev[devs.len].cod = cod;
                break;
            }

            /* RSSI */
            case ESP_BT_GAP_DEV_PROP_RSSI:
            {
                int8_t rssi = *(int8_t *)(p[i].val);
                devs.dev[devs.len].rssi = rssi;
                break;
            }

            /* 设备名 */
            case ESP_BT_GAP_DEV_PROP_BDNAME:
            {
                if (p[i].len > 0) {
                    int8_t* bdname = (int8_t *)(p[i].val);
                    memcpy(devs.dev[devs.len].name, bdname, p[i].len > ESP_BT_GAP_MAX_BDNAME_LEN - 1 ? ESP_BT_GAP_MAX_BDNAME_LEN - 1 : p[i].len);
                }
                break;
            }

            /* 拓展数据 */
            case ESP_BT_GAP_DEV_PROP_EIR:
            {
                if (strlen(devs.dev[devs.len].name) == 0 && p[i].len > 0) {
                    uint8_t* eir = (uint8_t *)(p[i].val);
                    uint8_t len = 0;
                    uint8_t* bdname = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_CMPL_LOCAL_NAME, &len);
                    if (!bdname) {
                        bdname = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_SHORT_LOCAL_NAME, &len);
                    }
                    if (bdname) {
                        len = len > ESP_BT_GAP_MAX_BDNAME_LEN - 1 ? ESP_BT_GAP_MAX_BDNAME_LEN - 1 : len;
                        memcpy(devs.dev[devs.len].name, bdname, len);
                    }
                }
                break;
            }

            default:
                break;
        }
    }

    devs.len++;
}

static void update_device_service(int index)
{
    ESP_LOGI(TAG, "Query service for device " BDASTR, BDA2STR(devs.dev[index].bda));
    esp_bt_gap_get_remote_services(devs.dev[index].bda);
}

static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
    static int index = 0;

    switch (event) {
        /* 设备搜索结果 */
        case ESP_BT_GAP_DISC_RES_EVT:
        {
            if (devs.len < MAX_DEV_NUM) {
                update_device_info(param);
            }
            break;
        }

        /* 搜索状态改变 */
        case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
        {
            if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) {
                ESP_LOGI(TAG, "Discoveried %d devices", devs.len);
                if (devs.len) {
                    update_device_service(index);
                } else {
                    xEventGroupSetBits(event_group, 0x01);
                }
            } else if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STARTED) {
                ESP_LOGI(TAG, "Discoverying devices...");
            }
            break;
        }

        /* 服务询问结果 */
        case ESP_BT_GAP_RMT_SRVCS_EVT:
        {
            if (param->rmt_srvcs.stat == ESP_BT_STATUS_SUCCESS) {
                uuid_list_t *uuids = &devs.dev[index].uuids;
                ESP_LOGI(TAG, "Get %d UUIDs", param->rmt_srvcs.num_uuids);
                int num = param->rmt_srvcs.num_uuids > MAX_UUID_NUM ? MAX_UUID_NUM : param->rmt_srvcs.num_uuids;
                for (int i = 0; i < num; i++, uuids->len++) {
                    memcpy(&uuids->uuid[i], &param->rmt_srvcs.uuid_list[i], sizeof(esp_bt_uuid_t));
                }
            }
            if (index++ < devs.len) {
                update_device_service(index);
            } else {
                xEventGroupSetBits(event_group, 0x01);
            }
            break;
        }

        default:
            break;
    }
}

int app_main()
{
    /* 初始化NVS */
    esp_err_t 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());
        ESP_ERROR_CHECK(nvs_flash_init());
    }

    /* 释放低功耗蓝牙资源 */
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));

    /* 初始化蓝牙 */
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) {
        ESP_LOGE(TAG, "initialize controller failed: %s", esp_err_to_name(ret));
        return -1;
    }

    /* 使能蓝牙控制器 */
    if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK) {
        ESP_LOGE(TAG, "enable controller failed: %s", esp_err_to_name(ret));
        return -1;
    }

    /* 初始化blueroid */
    esp_bluedroid_config_t bluedroid_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
    if ((ret = esp_bluedroid_init_with_cfg(&bluedroid_cfg)) != ESP_OK) {
        ESP_LOGE(TAG, "initialize bluedroid failed: %s", esp_err_to_name(ret));
        return -1;
    }

    /* 使能blueroid */
    if ((ret = esp_bluedroid_enable()) != ESP_OK) {
        ESP_LOGE(TAG, "enable bluedroid failed: %s", esp_err_to_name(ret));
        return -1;
    }

    /* 使能GAP协议 */
    const uint8_t *bda = esp_bt_dev_get_address();
    ESP_LOGI(TAG, "Own address: " BDASTR, BDA2STR(bda));

    /* 注册GAP回调 */
    esp_bt_gap_register_callback(bt_app_gap_cb);

    /* 设置设备名 */
    char *dev_name = "ESP32";
    esp_bt_gap_set_device_name(dev_name);

    /* 设置连接模式 */
    esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);

    event_group = xEventGroupCreate();
    while (1) {
        memset(&devs, 0, sizeof(bt_dev_t));
        /* 开始搜索周围设备 */
        esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 10, MAX_DEV_NUM);
        /* 等待搜索结束 */
        xEventGroupWaitBits(event_group, 0x01, pdTRUE, pdTRUE, portMAX_DELAY);
        /* 打印搜索结果 */
        print_device_info();

        vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
}

1. 初始化NVS。

        这个系统部件在WiFi那部分的文章讲过,就是用来储存协议栈的配置信息的。

2. 释放资源。

        因为这里只使用经典蓝牙,所以可以调esp_bt_controller_mem_release释放掉低功耗蓝牙部分的资源。

3. 初始化蓝牙控制器。

        先调esp_bt_controller_init初始化结构体,这里使用BT_CONTROLLER_INIT_CONFIG_DEFAULT宏;结构体的内容在前面menuconfig已经进行了配置,所以代码里面就不用配置了。然后调esp_bt_controller_enable使能控制器。

4. 初始化Bluedroid。

        先调esp_bluedroid_init_with_cfg初始化结构体,这里使用BT_BLUEDROID_INIT_CONFIG_DEFAULT宏,也是在menuconfig已经配置过了;然后调esp_bluedroid_enable使能Bluedroid。

5. 配置GAP协议。

        这个协议是用来发现周围设备的。调esp_bt_gap_set_device_name设置本设备的名字;调esp_bt_gap_set_scan_mode配置连接模式;调esp_bt_gap_register_callback设置回调函数。

        GAP的回调函数有非常多的事件,这里我只处理ESP_BT_GAP_DISC_RES_EVT(设备发现结果事件)、ESP_BT_GAP_DISC_STATE_CHANGED_EVT(设备发现状态改变事件)和ESP_BT_GAP_RMT_SRVCS_EVT(服务获取事件)。

        先说设备发现状态改变事件,当调用esp_bt_gap_start_discovery函数发起设备发现时,会触发一次这个事件,表示正在发现设备;当结束后,也会触发一次该事件,表示设备发现结束,同时会触发设备发现结果事件,返回搜索到的结果。服务获取事件的话,是在获取到对应设备服务后会触发。

        设备发现结果事件会返回下面的结构体:

struct disc_res_param {
    esp_bd_addr_t bda;
    int num_prop;
    esp_bt_gap_dev_prop_t *prop;
} disc_res;
  • bda:蓝牙设备的地址,类似MAC地址;
  • num_prop:属性的数量;
  • prop:属性列表,每个单元如下。 
typedef struct {
    esp_bt_gap_dev_prop_type_t type;
    int len;
    void *val;
} esp_bt_gap_dev_prop_t;
  • type:属性类型,有如下;
typedef enum {
    ESP_BT_GAP_DEV_PROP_BDNAME = 1,                 /*!< Bluetooth device name, value type is int8_t [] */
    ESP_BT_GAP_DEV_PROP_COD,                        /*!< Class of Device, value type is uint32_t */
    ESP_BT_GAP_DEV_PROP_RSSI,                       /*!< Received Signal strength Indication, value type is int8_t, ranging from -128 to 127 */
    ESP_BT_GAP_DEV_PROP_EIR,                        /*!< Extended Inquiry Response, value type is uint8_t [] */
} esp_bt_gap_dev_prop_type_t;
  • len:属性长度;
  • val:属性值。

        在设备发现结束后,会逐一询问设备的服务类型,回调函数返回的结构体如下:

struct rmt_srvcs_param {
    esp_bd_addr_t bda;
    esp_bt_status_t stat;
    int num_uuids;
    esp_bt_uuid_t *uuid_list;
} rmt_srvcs;
  • bda:设备地址;
  • stat:设备搜索状态;
  • num_uuids:UUID数量;
  • uuid_list:UUID列表。 

6. 主函数

        主函数中就是调用前面说到的esp_bt_gap_start_discovery函数发起设备发现,参数一表示发现类型,保持默认(ESP_BT_INQ_MODE_GENERAL_INQUIRY)即可;参数二表示发现时长,单位为1.28s;参数三可以定义最多发现几个设备,0的话代表不限制。

        后面我用了一个EventGroup来等待设备发现和设备服务查询结束,之后就是打印搜索到的设备信息。等待5秒,然后又继续下一轮的查询。

在服务查询的过程中可能会弹出很多的警告和错误,但一般不会影响程序的运行。

很抱歉,我无法直接为您提供完整的协议栈代码。协议栈的实现可能因硬件和操作系统不同而有所不同,需要根据具体情况进行适当的修改。 以下是一些常用协议栈的简介及其相关资源,供您参考: 1. WIFI协议栈:WIFI协议栈主要包括PHY层、MAC层和网络层。常用的WIFI协议包括IEEE 802.11a/b/g/n/ac/ax等。如果您需要实现WIFI协议栈,您可以参考以下资源: - ESP8266 WIFI模块源代码:https://github.com/espressif/esp8266-rtos-sdk/tree/master/components/esp8266/include/driver/include/driver - ESP32 WIFI模块源代码:https://github.com/espressif/esp-idf/tree/master/components/esp_wifi - Linux下的WIFI实现:https://wireless.wiki.kernel.org/en/developers/documentation/wireless-drivers 2. 蓝牙协议栈蓝牙协议栈主要包括PHY层、MAC层、L2CAP层、RFCOMM层、SDP层等。常用的蓝牙协议包括BLE、Classic Bluetooth等。如果您需要实现蓝牙协议栈,您可以参考以下资源: - BlueZ蓝牙协议栈:https://git.kernel.org/pub/scm/bluetooth/bluez.git/ - Android蓝牙协议栈:https://android.googlesource.com/platform/system/bt/ - Nordic Semiconductor的nRF5 SDK:https://www.nordicsemi.com/Software-and-Tools/Software/nRF5-SDK 3. TCP/IP协议栈:TCP/IP协议栈主要包括物理层、数据链路层、网络层、传输层和应用层。常用的TCP/IP协议包括TCP、UDP、IP等。如果您需要实现TCP/IP协议栈,您可以参考以下资源: - lwIP协议栈:http://www.nongnu.org/lwip/ - Linux内核中的TCP/IP协议栈实现:https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt - Contiki OS中的TCP/IP协议栈实现:https://github.com/contiki-os/contiki/tree/master/os/net 4. CoAP协议栈:CoAP协议栈是一种轻量级的RESTful协议,适用于物联网设备之间的通信。如果您需要实现CoAP协议栈,您可以参考以下资源: - Erbium CoAP协议栈:https://github.com/contiki-os/er-coap - LibCoAP协议栈:https://github.com/obgm/libcoap - Californium CoAP协议栈:https://github.com/eclipse/californium 5. MQTT协议栈:MQTT协议栈是一种轻量级的消息协议,适用于物联网设备之间的通信。如果您需要实现MQTT协议栈,您可以参考以下资源: - Paho MQTT协议栈:https://github.com/eclipse/paho.mqtt.embedded-c - Mosquitto MQTT协议栈:https://github.com/eclipse/mosquitto - Eclipse IoT MQTT协议栈:https://www.eclipse.org/paho/clients/c/embedded/ 6. HTTP(S)协议栈:HTTP(S)协议栈是一种广泛应用于互联网上的应用层协议。如果您需要实现HTTP(S)协议栈,您可以参考以下资源: - Mongoose HTTP(S)协议栈:https://github.com/cesanta/mongoose - libcurl HTTP(S)协议栈:https://curl.se/libcurl/ - Microchip HTTP(S)协议栈:https://www.microchip.com/design-centers/wireless-connectivity/wifi/products/wi-fi-software/mrf24w-software 7. SSL/TLS协议栈:SSL/TLS协议栈是一种安全传输协议,适用于互联网上的安全通信。如果您需要实现SSL/TLS协议栈,您可以参考以下资源: - OpenSSL SSL/TLS协议栈:https://www.openssl.org/ - wolfSSL SSL/TLS协议栈:https://www.wolfssl.com/ - mbed TLS SSL/TLS协议栈:https://tls.mbed.org/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马浩同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值