使用ESP32-S3开发蓝牙BLE5.0低功耗模式。蓝牙线BLE协议栈整体层级如下:

物理层(PHY) → 链路层(LL) → L2CAP → ATT → GATT → GATT if → GATT app

使用ESP-IDF V5.5.1开发整体流程如下:

NVS Flash初始化 -> 蓝牙控制器配置 -> 蓝牙协议栈配置 -> 注册GAP、GATT、APP回调函数。

具体操作如下:

   1. 使用nvs_flash_init()初始化NVS Flash,这里保存了协议栈的配置

   2. 蓝牙控制器释放内存 -> 初始化 -> 使能:

      系统默认使用的Bluedroid主机协议,如果不是需要更改,需要在menuconfig菜单里重新配置:

    menuconfig -> Component config -> Bluetooth -> Host里选择

      2.1 使用esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)释放经典蓝牙控制器内存

      2.2 使用esp_bt_controller_init(bt_cfg)初始化蓝牙控制器,使用默认参数

      esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();

      2.3 使用esp_bt_controller_enable(ESP_BT_MODE_BLE)使能控制器低功耗蓝牙模式

      ESP_BT_MODE_BTDM:表示蓝牙双模,BLE+BT

   3. 蓝牙协议栈Bluedroid初始化 -> 使能

      3.1 使用esp_bluedroid_init()初始化Bluedroid蓝牙协议栈

      3.2 使用esp_bluedroid_enable()使能Bluedroid蓝牙协议栈

    到目前阶段,蓝牙协议栈已经配置并使能,但是GAP和GATT层并没有配置,蓝牙未启动,因此不会发送广播。

*****************************************以上都是用默认配置即可*****************************************

    以下3个事件回调注册函数必须在使用esp_bluedroid_enable()后才能调用,因为以下三个函数里都使用ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED)检查Bluedroid堆栈是否使能,使能后才能继续注册。

   4. 使用esp_ble_gap_register_callback()注册GAP服务事件回调函数,只是将回调函数指针保存到Bluedroid协议栈的GAP层的队列中,不会引发任何事件。

   5. 使用esp_ble_gatts_register_callback()注册GATT服务回调函数,只是将回调函数指针保存到

   Bluedroid协议栈的GATT层的队列中,不会引发任何事件。

   6. 使用esp_ble_gatts_app_register(ESP_APP_ID)注册应用配置文件,该函数内部有通知消息,等注册完成后会异步触发ESP_GATTS_REG_EVT事件(原始事件触发点)。

注意:第4和5顺序可以互换,最后一步注册APP。

esp_ble_gatts_app_register() ->  程序内部向 BTC 层发送「注册请求消息->

蓝牙底层处理请求 -> 底层通过 BTC 层反向触发gatt注册的回调函数 ->

运行gatts_event_handler()回调函数 -> ESP_GATTS_REG_EVT 事件第一次处理 -> gl_profile_tab[]先注册(由用户声明并初始化)-> 运行gl_profile_tab[idx].gatts_cb()函数 ->  gatts_profile_a_event_handler -> event = ESP_GATTS_REG_EVT 开始配置基本服务(第二次处理,事件还未更新),在ESP_GATTS_REG_EVT里一并触发GAP和GATT配置服务:

        6.1 配置GAP服务

            6.1.1 使用esp_ble_gap_set_device_name()设置设备名称;

            6.1.2 先配置广播参数(在profile_a_event_handler函数里),配置完后会触发ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT事件

            使用esp_ble_gap_config_adv_data()配置标准广播数据/广播响应数据参数

            或者esp_ble_gap_ext_adv_set_params()配置扩展广播参数

            这是GAP服务首个触发点,会自动触发以下的GAP链式任务;

            6.1.3 ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT事件响应任务(在gap_event_handler()函数里),配置广播参数,

            配置完后会触发ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT事件

            使用esp_ble_gap_config_adv_data_raw()配置广播原始数据,

            或者使用esp_ble_gap_config_scan_rsp_data_raw()配置广播响应原始数据,

            esp_ble_gap_config_ext_adv_data_raw()扩展广播原始数据配置

            6.1.4 ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT事件相应任务(在gap_event_handler()函数里),开启广播后,会触发        ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT事件,在对应的事件里,使用esp_ble_gap_start_advertising()开启广播数据;            esp_ble_gap_ext_adv_start()扩展广播开始,从现在开始广播。

            6.1.5 ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT事件响应事件,用户可以添加自己代码判断广播是否正常启动

        6.2 配置GATT服务 ESP_GATTS_REG_EVT -> ESP_GATTS_CREATE_EVT -> ESP_GATTS_START_EVT,所有操作需基于 GATTS 事件回调触发,不可同步连续调用;

        GATT 服务的构建遵循以下流程:

创建服务(esp_ble_gatts_create_service())
    ↓
添加特征(esp_ble_gatts_add_char())【可多次调用,添加多个特征】
    ↓
添加描述符(可选,esp_ble_gatts_add_char_descr())
    ↓
启动服务(esp_ble_gatts_start_service())

            6.2.1 使用esp_ble_gatts_create_service()创建GATT服务,GATT服务首个触发点,然后触发ESP_GATTS_CREATE_EVT事件;

            6.2.2 在ESP_GATTS_CREATE_EVT事件响应任务,使用esp_ble_gatts_start_service()启动已创建的服务。并触发ESP_GATTS_START_EVT事件;使用esp_ble_gatts_add_char()添加特征值,并触发

            6.2.3 在ESP_GATTS_START_EVT 事件响应任务,GATT服务激活结果反馈事件响应任务,基于服务句柄调用 esp_ble_gatts_add_char() 添加特征值,

            系统完成特征值初始化后,触发 ESP_GATTS_ADD_CHAR_EVT;

            6.2.4 (可选)基于特征值句柄调用 esp_ble_gatts_add_char_descr() 添加描述符(如 CCCD)。用户添加代码,判断GATT服务是否启动成功。

*****************************************整个的配置完成并运行*****************************************

蓝牙协议栈是事件驱动型程序,以下事件与用户息息相关:

gatts_if:GATT服务器接口标识(类型uint8_t)

作用:作为GATT服务器实例的唯一标识,后续调用GATT相关API时(esp_ble_gatts_create_attr_tab()创建服务、

esp_gatts_send_response()发送响应等),需传入此参数指定操作的GATT服务器实例。如果系统中存在多个GATT服务器

实例(较少见),每个实例会有独立的 gatts_if,分别通过各自的 ESP_GATTS_REG_EVT 事件返回。

    获取:gatts_if是由系统自动分配的,并非通过主动调用某个函数直接分配,而是在注册GATT服务回调函数后,通过

事件回调返回。应用层需在该事件中保存 gatts_if 供后续使用。

    1. 调用esp_ble_gatts_register_callback()注册GATT服务器的全局回调函数后,系统会自动分配gatts_if,

    2. 通过ESP_GATTS_REG_EVT事件将其返回给应用层,

        2.1 可以直接赋值获取:esp_gatt_if_t GattsIf = gatts_if;

        2.2 esp_ble_gatts_create_attr_tab()主要作用是创建并注册GATT属性表,将GATT服务实例gats_if与属性表

    绑定,使客户端能够通过BLE发现这些属性,进而进行后续的读、写、通知、订阅等操作。

Attribute属性由4部分组成:Handle(句柄),UUID,Permissions(权限)、Value(值)

Characteristic特征数据由特征数据声明、特征数据值和特征数据描述符组成

    特征声明的UUID为0x2803,其Value字段包含:

        1. 特征属性,读、写、通知等,由 esp_gatt_char_prop_t 定义,1字节;

        2. 特征值的句柄,2字节;

        3. 特征值的UUID,根据UUID类型不同,长度不同;

    特征描述符属性:

        1. 用户特征值描述符的UUID为0x2901,用于对特征值的描述(如字符串"Temp Value"),帮助客户端理解特征值含义

        2. 客户端特征值配置描述符的UUDI为0x2902,用于控制服务器是否向客户端发送通知或指示,其Value字段常为2字节,

        0x0000表示关闭,0x0001表示开启通知。

服务由服务声明、服务包含、特征值、描述符组成

    服务声明UUID为0x2800,用于表示一个主要服务,其Value字段存储改服务的UUID

    次要服务声明UUID为0x2801

    特征值

GATT常用事件:

    ESP_GATTS_READ_EVT 读事件:客户端向GATT服务器发起读特征值请求时,服务器端会触发该事件

    ESP_GATTS_WRITE_EVT 写事件:客户端向GATT服务器的特征值写入数据时,服务器端会触发该事件。

        开发者可以在此回调函数中获取客户端写入的数据并处理。有两种方式写入特征值:

            1. Write Request:客户端发送写入请求,服务器处理后需回复,叫做"带响应的写

            2. Write Command:客户端发送写入命令,服务器不用回复,叫做"不带响应的写"。

        需要处理的是esp_ble_gatts_cb_param_t参数里的以下数据结构:

        struct gatts_write_evt_param

        {

            uint16_t conn_id;              

            uint32_t trans_id;              

            esp_bd_addr_t bda;              

            uint16_t handle;                

            uint16_t offset;              

            bool need_rsp;                  

            bool is_prep;                  

            uint16_t len;                  

            uint8_t *value;                

        } write;  

服务器主动向客户端推送数据的两种机制:

    通知Notify:服务器发送数据后,不需要客户端确认,0x0001启用

    指示Indicate:服务器发送数据后,必须等待客户端确认,0x0002启用

    都需要客户端发起订阅操作,客户端必须先通过写入特征的CCCD值后,服务器才能向其推送数据

    两都用esp_ble_gatts_send_indicate()发送数据,通过参数need_confirm=false/ture区分

GATT(Generic Attribute Profile)定义了一种用于数据交换的分层结构,包括Profile(通用属性配置文件)、Service(服务)、Characteristic(特征值)、Descriptor(描述符)等。一个BLE设备可以有多个服务,每个服务可以包含多个特征值,每个特征值可以有零个或多个特征描述符。

Profile(配置文件)定义了BLE设备的功能和数据交换规则。包括关键概念有:服务、特征值、特征描述符、属性、配置文件角色、数据交换格式、安全和配对等。

Service(服务)主要用来组织数据而不是存储数据,是一组特征值的组合,每个服务都有一个唯一的标识符UUID,使得客户端可以标准化识别服务。主要分为主服务、次服务和包含服务。服务可以包含多个特征值,特征值可以包含多个特征描述符(Descriptors)。服务本身没有直接的访问权限,但它们包含的特征值可以具有不同的访问权限,如只读、可写、可通知等。

Characteristic(特征)由特征声明,特征值,特征描述符组成。每个特征都有一个唯一的UUID(Universally Unique Identifier),用于在GATT数据库中标识不同的特征值。特征值是客户端和服务器之间数据交换的基本单元。其表示BLE设备中的一个数据项,可以是温度传感器的读数、设备的电池电量、或者其它用户自定义的数据设置通道。客户端可以读取服务器的特征值,或者在特征值上注册通知,以接收数据更新。

  • 其定义了一系列特征属性(Properties),这些属性描述了特征值的行为,例如是否可读、可写、可通知(Notify)、可指示(Indicate)等。

  • 特征值可以有零个或多个特征描述符(Descriptor),这些描述符提供了关于特征值的额外信息,例如用户友好的描述、配置参数等。

  • 特征值具有访问权限,定义了哪些操作(如读取、写入)是被允许的。这些权限可以用于实现安全措施,如认证和加密。

  • 特征值可以是动态的,其值随设备状态变化而变化;也可以是静态的,其值在设备使用期间保持不变。

特征声明定义了一个特征的基本信息,包括特征的 UUID、权限(例如读、写、通知等)、属性类型等。它是特征的标识和描述。(特征声明的UUID一般是0x2803)。特征声明有一个值,这个值有5个字节:包括特征的属性(0字节),特征值句柄(1,2字节),和特征UUID(3,4字节);

Properties(特征值的属性)

定义位置:在特征声明(Characteristic Declaration,UUID: 0x2803)中声明
作用:定义客户端(如手机)可以对特征值执行的操作类型(公开声明,客户端可见)。

具体属性值及含义: 1字节的位掩码(Bitmask),每个位表示一种支持的操作

属性位(十六进制) 名称 说明
0x01 Broadcast 允许通过广播发送特征值(需配合广播数据包配置)。
0x02 Read 允许客户端通过读请求(Read Request)主动读取特征值。
0x04 Write Without Response 允许客户端通过无响应写入(Write Without Response)发送数据,无需服务器确认。
0x08 Write 允许客户端通过写入请求(Write Request)修改特征值,需服务器响应确认。
0x10 Notify 允许服务器通过通知(Notification)主动推送数据,无需客户端确认。
0x20 Indicate 允许服务器通过指示(Indication)主动推送数据,需客户端确认。
0x40 Authenticated Signed Writes 写入操作需使用认证签名(增强安全性)。
0x80 Extended Properties 表示特征包含扩展属性描述符。

权限(Permissions)

作用:控制客户端对特征值的访问规则(服务器端安全配置,客户端不可见)。
定义位置:在特征值属性(Characteristic Value)和描述符(Descriptors)中设置。

具体权限类型

权限标志 说明
Readable 允许客户端读取特征值。
Writable 允许客户端通过 Write Request 写入特征值(需服务器确认)。
Write Without Response 允许客户端通过 Write Without Response 写入(无需确认)。
Encrypted Read 需加密连接(如LE Secure Connection)才能读取。
Encrypted Write 需加密连接才能写入。
Authenticated Read 需配对认证(如MITM保护)才能读取。
Authenticated Write 需配对认证才能写入。
Authorization Required 需应用层授权(如用户手动确认)。
No Access 完全禁止访问(仅服务器内部使用)。

属性(Properties)与权限(Permissions)两者共同决定客户端能否成功执行操作:
属性是“菜单”:客户端根据属性判断可以尝试哪些操作(如是否发送读请求)。
权限是“门禁”:服务器根据权限决定是否允许操作(如拒绝未加密的读请求)

UUID:通用唯一识别码,包括协议标识符、浏览组标识符、服务SDP服务类标识符与规范标识符、服务、单位、特征声明、特征值、特征描述符、对象类型、SDO服务、成员服务(主要是公司名称)、mesh协议等,都有各自的UUID。官方标准的UUID为16位 ,其完整形式为 0000XXXX-0000-1000-8000-00805F9B34FB。

  • 唯一性:UUID提供了一个全球唯一的标识符,确保了不同设备或开发者定义的服务、特征值和描述符不会发生冲突。

  • 标准化:使用UUID有助于标准化,使得不同制造商生产的设备能够通过标准化的接口进行通信和交互。

句柄: 服务句柄(Service Handle)是一个用于唯一标识服务的数字。每个服务、特征值(Characteristic)、以及特征描述符(Descriptor)在GATT数据库中都有一个唯一的句柄值。

  • 在GATT数据库,即profile_data中,句柄值通常会从1开始,然后依次增加。
  • 服务句柄通常与其他属性的句柄一起定义了一个服务的范围,即服务的第一个和最后一个属性句柄标识了服务的开始和结束。
  • 服务句柄与服务的UUID相关联,UUID用于定义服务的类型和功能,而句柄用于在GATT数据库中标识服务的位置。

主服务: 主服务是GATT数据库中的顶级服务,它作为GATT层次结构的起点,包含多个特征值(Characteristics)和次级服务(Secondary Services)。

  • 一个主服务可以包含多个次级服务,而次级服务也可以进一步包含其他次级服务或特征值。

  • 主服务可以被客户端设备发现,客户端设备通过扫描和查询来识别服务器端提供的服务

    在BLE设备配对和连接过程中,客户端设备首先发现主服务,然后才能进一步发现和访问服务内的特征值

  • 主服务在GATT数据库中定义了一个服务范围,由服务的起始句柄(Start Handle)和结束句柄(End Handle)标识

ATT(属性协议)定义了一个叫attribute数据结构,他是整个数据通信中最小的数据单元。该数据结构由下列四个字段组成:

  • 属性句柄 (Attribute Handle)
    定义:一个 16 位(2 字节)的无符号整数,取值范围为 0x0001 到 0xFFFF。
    作用:作为属性在属性表中的唯一标识符和地址。它类似于数据库中的主键 (Primary Key)。
    特性:
    • 唯一性:在一个设备(GATT 服务器)的属性表中,每个句柄都是唯一的。
    • 有序性:句柄值是严格递增的。句柄之间可以有空隙,只要保证句柄值是按照递增顺序存储的即可。
    • 高效性:在设备连接后,客户端和服务器之间的所有数据读写操作都通过这个简短的句柄来指定目标属性,而不是使用冗长的 UUID,从而极大地提高了通信效率和降低了功耗。
  • 属性类型 (Attribute Type)
    定义:一个 UUID (Universally Unique Identifier),用于说明该属性中“值 (Value)”代表的是什么。它类似于数据库中的列类型或模式 (Schema)。
    作用:为属性赋予语义。客户端通过识别类型 UUID 来理解一个属性的用途。
    分类:
    • 官方定义 (SIG-defined) UUID:16 位短 UUID,用于表示标准的服务、特征和描述符。
    • 自定义 (Vendor-specific) UUID:128 位长 UUID,用于厂商自定义的私有服务和特征。
  • 属性值 (Attribute Value)
    定义:属性所承载的实际数据,是一个长度可变的字节序列。
    作用:存放应用程序的数据或协议的结构化信息。
    特性:
    • 格式自由:ATT/GATT 协议本身不关心值的具体内容,它只负责传输字节流。如何解析这些字节(例如,解析成一个整数、一个字符串或一个复杂的结构体)完全由应用层负责
    • 长度可变:值的长度可以从 0 字节到 ATT_MTU (最大传输单元) 限制的字节数。
  • 属性权限 ( Attribute Permissions)
    定义:一组标志位,用于规定客户端可以对该属性执行哪些操作。
    作用:为属性提供安全和访问控制
    常见的权限:
    • 读 (Read):属性值可被读取。
    • 写 (Write):属性值可被写入。
    • 无响应写 (Write Without Response):可被写入,且服务器不发送响应。
    • 认证读/写 (Authenticated Read/Write):需要经过加密和认证的安全连接才能读/写。
    • 授权读/写 (Authorization Read/Write):服务器应用层需要对每次读/写请求进行授权。
      注意:权限本身不是属性值的一部分,而是与句柄关联的、由服务器协议栈管理的元数据。

Attribute访问方式

ATT协议还定义了读写属性的方式。具体的方式取决于发起属性访问过程的是客户端还是服务端。
客户端发起属性访问,有两种操作——Write和Read

客户端使用Read从服务器读取属性的值,服务器响应属性的值。
客户端使用Write将一个属性的值写入服务器,服务器响应写操作是否成功。
②服务端发起属性访问,有两种操作——Notification和Indication

  • Notification:当Attribute发生改变时,服务端使用该种方式向客户端发送更新后的属性值,客户端收到后不响应此操作
  • Indication:与Notification类似,但是客户端必须发送是否正确收到该Attribute的响应

注意:Attribute handleAttribute type是公共信息,而Attribute ValueAttribute Permissions是私有信息。

BLE传输是采用小端模式,即低字节(数值的低位)存放在内存低地址 / 传输流的前位,高字节(数值的高位)存放在内存高地址 / 传输流的后位。

GATT注册回调函数的原型:

typedef void (* esp_gatts_cb_t)(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);

event:GATT事件,enum格式,预定义

gatts_if: 接口,uint8_t

param: 参数,union,不同的事件对应不同的参数

gatts_if 与 GATT App 的核心关系

gatts_if(GATT Server Interface)和 GATT App(GATT 应用)是一一对应的绑定关系,一个 App 对应一个 gatts_if。调用 esp_ble_gatts_app_register(app_id) 注册 GATT App 时,协议栈会为该 App 分配唯一的 gatts_if,并通过 ESP_GATTS_REG_EVT 事件返回。:

  • GATT App:是 ESP32 蓝牙协议栈中对 “一个独立 GATT 服务器实例” 的逻辑抽象,每个 App 对应一套完整的 GATT 服务 / 特征 / 描述符体系;

  • gatts_if:是协议栈为每个 GATT App 分配的唯一整数标识(句柄),用于在代码中定位、操作对应的 GATT App 实例,是操作 App 的唯一凭证。

简单理解:GATT App 是 “实体”(一套独立的 GATT 服务体系),gatts_if 是 “身份证号”(操作该实体的唯一凭证)。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐