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、Baseband、Link Controller、Link Manager、Device Manager、HCI等模块,用于硬件接口管理、链路管理等等;主机则包括了L2CAP、SMP、SDP、ATT、GATT、GAP以及各种规范,构建了向应用层提供接口的基础,方便应用层对蓝牙系统的访问。
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], ¶m->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秒,然后又继续下一轮的查询。
在服务查询的过程中可能会弹出很多的警告和错误,但一般不会影响程序的运行。