Arduino 入门学习笔记(三十):BLE_UART 实验

开发板:正点原子ESP32S3
没有LCD屏可以用串口打印进行测试
例程源码在文章顶部可免费下载

1. 蓝牙基础知识和蓝牙通信函数介绍

在上一实验已经讲解了不少蓝牙的基础知识了,本实验针对性讲解蓝牙设备连接上之后的数据通信。 这部分内容主要涉及到属性协议、通用属性协议的内容。 通用属性协议 GATT 就好比一个公司的出纳和库房前台,属性协议 ATT 就是库房。出纳负责处理向上与应用打交道,而库房前台负责向下把检索任务子进程交给 ATT 库房去做,库房则负责数据检索。
属性协议是用于发现、读取和写入对端设备上的属性的规范。属性协议采用了一种客户端服务器模式。服务器暴露一系列属性给客户端。这些属性可以由客户端发现、读取和写入,也可以由服务器指示和通知。

1.1 属性介绍

属性代表着数据,设备在任意状态任意事件所对应的相关数据。它可以是位置、尺寸、质量、温度、速度或某个设备向分享给其他设备的任何数据。属性协议用来向远端设备推送或获取数据。 此外,属性协议也支持设置通知和指示,当数据发生变化时远端设备可以获得警报。
例如,温度计所测得的温度,温度的单位、设备的名称型号及设备制造商的名称等都是属性。属性由 4 个元素组成:属性句柄、属性类型、属性值和属性许可。
属性句柄:与使用内存地址查找内存中的内容一样,通过属性句柄以找到相应的属性。例如,第一个属性的句柄是 0x0001,第二个属性的句柄是 0x0002,以此类推,属性句柄最大可以为 0xFFFF。
属性类型:每个数据有自己需要代表的意思,例如,表示温度、发射功率、电量等各种各样的信息。蓝牙组织 Bluetooth SIG 对常用的一些数据类型进行了归类,赋予不同的数据类型不同的通用唯一识别码(Universally Unique Identifier, UUID)。例如, 0x2A09 表示电池信息,0x2A6E 表示温度信息。 UUID 可以是 16 位,也可以是 128 位。
属性值:属性值是每个属性真正要承载的信息,其他 3 个元素都是为了让对方能够更好地获取属性值。有些属性的长度是固定的,例如,电池属性的长度只有 1 个字节,因为需要表示的数据仅为“0 ~ 100%”,而 1 字节足以表示 1 ~ 100;而有些属性的长度是可变的,如基于 BLE实现的透明传输模块。
属性许可:每个属性对各自的属性值有相应的访问限制,例如,有些属性是可读的,有些是可写的,有些是可读又可写的等等。拥有数据的一方可以通过属性许可控制本地数据的可读/写属性。
存有数据(即属性)的设备称为服务器端,获取数据的设备称为客户端。
下面是服务器端和客户端间的常用操作:
① 客户端给服务器端发送数据,对服务器端的数据进行写操作(写操作分为两种,一种是写入请求,另一种是写入命令,两者的主要区别是前者需要对方回复,而后者不需要)
② 服务器端给客户端发送数据,主要是通过服务器端指示或者通知的形式,将服务器端更新的数据发送给客户端(与写操作类似,指示和通知的主要区别是前者需要对方回复确认)。
③ 客户端也可以主动通过读操作读取服务器端的数据。服务器端和客户端的交互操作都是通过消息 ATT 的协议数据单元(Protocol Data Unit, PDU)实现的。每个设备可以指定自己支持的最大消息长度。

1.2 属性协议 AAT 介绍

属性协议定义了一个设备如何发现、读取和写入另外一台设备的属性。属性服务器暴露一系列属性及其属性值给一个匹配设备。属性客户端可以发现、读取和写入服务器中的属性,也可以被服务器指示或通知。
属性协议支持数据从客户端传向服务器,也支持从服务器传向客户端。客户端可以读写服务器的属性,而服务器也可以给客户端送属性。
属性协议数据单元有 6 种方法类型:
请求:由客户端向服务器发送,并调用响应。
响应:由服务器向客户端发送,响应某个请求。
命令:由客户端向服务器发送,且无响应。
通知:服务器未经请求向客户端发送。
指示:由服务器未经请求向客户端发送,并调用确认。
确认:由客户端向服务器发送,确认收到了一个指示。

1.3 通用属性协议 GAAT 介绍

属性协议 ATT 是用于发现、读取和写入远端设备上的属性的规范,而通用属性协议 GATT则是构建在属性协议 ATT 的基础上,定义了一个基于属性协议的服务框架,定义服务的流程、格式及其包含的特征,该流程包括特征的发现、读取、写入、通知、指示以及广播的配置。
通用属性协议 GATT 定义了一系列程序,用来发现服务、特征、服务之间的关系,以及用来读取和写入特征值。 GATT 结构如下图所示。
在这里插入图片描述
Profile 规范是一个由一个或多个服务构成,它们结合在一起实现某一个特定功能。
Service 服务是一个数据和相关行为的集合,以实现特定的功能或部分设备的特征。 每个Service 都有一个 UUID 唯一标识。 UUID 有 16bit 的,也有 128bit 的。 16bit 的 UUID 是官方通过认证的,需要花钱购买, 128bit 是自定义的,这个就可以随便设置。 为了适应复杂的行为和服务之间的关系,服务分为两种类型:主要服务和次要服务。服务的类型通常不取决于服务本身,而取决于设备如何使用该服务。
Characteristic 特征是一个由属性值、属性权限、 描述符组成的集合,是服务的组成部分之一。 与 Service 类似,每个 Characteristic 也会用 16bit 或 128bit 的 UUID 唯一标识。 属性和配置信息表明数据是如何访问以及数值如何进行表示和显示,通常一个特性的定义包含属性权限、属性值和描述符 3 个部分。
例如,在一间公司里有 300 名员工,为了使他们的工作更有效率,现在将把他们划分为不同部门、次级部门。属性协议 ATT 规定了众多属性及属性之间的关联操作,可以把这公司里的300 名员工比喻为属性协议 ATT。通用属性协议 GATT 将这些属性及其相关操作编入不同的规范 Profile、服务 Service 和特征 Characteristic。为了提高工作效率、方便管理,公司将这 300 名员工划分为若干个部门。规范就如同部门,例如后勤部或人力资源部,每个部门都相互独立并有其特定的职能。每个规范可以是一个或多个服务,就像后勤部能够实现物资管理、后勤保障等职能一样。每个服务都包含若干个次级服务或特征。这里把次级服务比作次级部门,例如后勤部的物资管理处等。而特征就如同部门里实现部门职能的个体,例如相关工作人员。
总之,通用属性协议 GATT 将功能类似的属性进行整合,将功能相关的属性整合到一个服务中,例如将和温度检测相关的属性整合为温度检测服务。而一个规范包含一个或多个这样的服务。

1.4 ESP32 GATT 介绍

基于 GATT 原理的介绍, ESP32 GATT 的实现会与很多库相关,如下图所示:
在这里插入图片描述
设备角色:客户端(BLEClient)、服务器(BLEServer)。
GATT 服务相关:本地服务(BLEService、 BLEServiceMap)、远端服务(BLERemoteService)。
GATT 服务下特征相关:本地特征(BLECharacteristic、 BLECharacteristicMap)、远端特征(BLERemoteCharacteristic)。
GATT 服务下特征描述符相关:本地特征描述符(BLEDescriptor、 BLEDescriptorMap)、远端特征描述符(BLERemoteDescriptor)。
GATT 服务下特征下的特征值相关: BLEValue。
以我们本实验要实现的功能来说, ESP32-S3 是作为一个服务器,被手机的蓝牙助手访问,所以这里会涉及到 BLEServer 的学习。作为服务器,其工作内容就是创建服务以及提供服务,所 以会 涉及 到 BLEService 、 BLEServiceMap 、 BLECharacteristic 、 BLECharacteristicMap 、BLEDescriptor、 BLEDescriptorMap。

1.5 蓝牙通信函数介绍

本节主要对 BLEDevice 库做介绍,其他库的介绍可以自行查看一下源文件。
BLEDevice 作为蓝牙 BLE 的中心控制者,主要是四大功能:基础功能、广播(Advertising)、扫描(Scan)、数据通信(GATT)。
在这里插入图片描述
本小节介绍到的函数可在以下文件中找到:
C:\Users\ 用户名 \AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.11\libraries\BLE\src\BLEDevice.cpp
C:\Users\ 用户名 \AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.11\libraries\BLE\src\BLEServer.cpp
C:\Users\ 用户名 \AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.11\libraries\BLE\src\BLECharacteristic.cpp
接下来,我们介绍一下本章节所用到的用到的函数。
第一个函数: init 函数,该函数功能是创建一个 BLE 设备。

void BLEDevice::init(std::string deviceName)

参数 deviceName 为 BLE 设备名称。
无返回值。
第二个函数: createServer 函数,该函数功能是创建 GATT 服务。

BLEServer* BLEDevice::createServer()

无参数;
返回值为 BLEServer 对象。
第三个函数: setCallbacks 函数,该函数功能是设置回调函数。

void BLEServer::setCallbacks(BLEServerCallbacks* pCallbacks)

参数 pCallbakcs 为要调用的函数;
无返回值。
第四个函数: createService 函数,该函数功能是创建服务。

BLEService* BLEServer::createService(const char* uuid)

参数 uuid 为通用唯一识别码;
返回值为 BLEService 对象。
第五个函数: createCharacteristic 函数,该函数的功能是创建特征对象。

BLECharacteristic* BLEService::createCharacteristic(const char* uuid, uint32_t
properties)

参数 uuid 为通用唯一识别码;
参数 properties 为特征的属性;
返回值为 BLECharacteristic 对象。
第六个函数: addDescriptor 函数,该函数的功能是加入特征描述符。

void BLECharacteristic::addDescriptor(BLEDescriptor* pDescriptor)

参数 pDescriptor 为特征 描述符;
无返回值。
第七个函数: start 函数,该函数的功能是启动服务。

void BLEService::start()

无参数;
无返回值。
第八个函数: getAdvertising 函数,该函数的功能是获取广播功能。

BLEAdvertising* BLESever::getAdvertising()

无参数;
返回值为 BLEAdvertising 对象。
第九个函数: setValue 函数,该函数的功能是清除扫描结果。

void BLECharacteristic::setValue(uint8_t* data, size_t length)

参数 data 为特征设置的数据;
参数 length 为数据长度;
无返回值。
第十个函数: notify 函数,该函数的功能是通知。

void BLECharacteristic::notify(bool is_notification)

参数 is_notification 为是否启动通知功能;
无返回值。
第十一个函数: startAdvertising 函数,该函数的功能是启动广播。

void BLEDevice::startAdvertising()

无参数;
无返回值。

2. 硬件设计

2.1 例程功能

程序下载完成, ESP32-S3 作为蓝牙设备,通过手机蓝牙助手进行连接后,就可以进行蓝牙通信。
手机蓝牙助手可以在手机软件商店下载“BLE 调试宝”或“BLE 调试助手” 。

2.2 硬件资源

  • LED 灯
    LED-IO1
  • USART0
    U0TXD-IO43 U0RXD-IO44
  • XL9555
    IIC_SDA-IO41 IIC_SCL-IO42
  • SPILCD
    CS-IO21 SCK-IO12 SDA-IO11
    DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
    PWR- IO1_3(XL9555)
    RST- IO1_2(XL9555)
  • ESP32-S3 内部 BLE

2.3 原理图

本章实验使用的 BLE 为 ESP32-S3 的片上资源,因此并没有相应的连接原理图。

3. 软件设计

3.1 程序流程图

下面看看本实验的程序流程图:
在这里插入图片描述

3.2 程序解析

05_ble_uart.ino 代码
在 05_ble_uart.ino 里面编写如下代码:

#include "led.h"
#include "uart.h"
#include "xl9555.h"
#include "spilcd.h"
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
BLEServer *pServer = NULL; /* BLEServer 指针 pServer */
BLECharacteristic * pTxCharacteristic;
BLECharacteristic * pRxCharacteristic;
bool deviceConnected = false; /* 本次连接状态 */
bool oldDeviceConnected = false; /* 上次连接状态 */
uint8_t txValue = 0;
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
class MyServerCallbacks: public BLEServerCallbacks
{
	void onConnect(BLEServer* pServer)
	{
		deviceConnected = true;
	};
	void onDisconnect(BLEServer* pServer)
	{
		deviceConnected = false;
	}
};
class MyCallbacks: public BLECharacteristicCallbacks
{
	void onWrite(BLECharacteristic *pCharacteristic)
	{
		std::string rxValue = pCharacteristic->getValue();
		if (rxValue.length() > 0)
		{
			lcd_show_string(30, 150, 290, 16, LCD_FONT_16, "rec value:", RED);
			for (int i = 0; i < rxValue.length(); i++)
			{
				lcd_fill(110 + i * 8, 150, 319, 168, WHITE);
				lcd_show_char(110 + i * 8, 150, rxValue[i], LCD_FONT_16, 0, BLUE);
			}
		}
	}
};

/**
* @brief 当程序开始执行时,将调用 setup()函数,通常用来初始化变量、函数等
* @param 无
* @retval 无
*/
void setup()
{
	led_init(); /* LED 初始化 */
	uart_init(0, 115200); /* 串口 0 初始化 */
	xl9555_init(); /* IO 扩展芯片初始化 */
	lcd_init(); /* LCD 初始化 */
	lcd_show_string(30, 50, 200, 16, LCD_FONT_16, "ESP32-S3", RED);
	lcd_show_string(30, 70, 200, 16, LCD_FONT_16, "BLE UART TEST", RED);
	lcd_show_string(30, 90, 200, 16, LCD_FONT_16, "ATOM@ALIENTEK", RED);
	BLEDevice::init("UART Service"); /* 创建一个 BLE 设备 */
	pServer = BLEDevice::createServer(); /* 创建一个 BLE 服务 */
	pServer->setCallbacks(new MyServerCallbacks()); /* 设置回调 */
	BLEService *pService=pServer->createService(SERVICE_UUID); /* 创建蓝牙服务器 */
	/* 创建发送特征,添加描述符,设置通知权限 / 创建接收特征,设置回调函数,设置可写权限 */
	pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX,
	BLECharacteristic::PROPERTY_NOTIFY);
	pTxCharacteristic->addDescriptor(new BLE2902());
	pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX,
	BLECharacteristic::PROPERTY_WRITE);
	pRxCharacteristic->setCallbacks(new MyCallbacks());
	pService->start(); /* 启动设备 */
	pServer->getAdvertising()->start(); /* 开始广播 */
	lcd_show_string(30, 110, 290, 16, LCD_FONT_16, "Waiting client connection to
	notify", RED);
	delay(500);
}

/**
* @brief 循环函数,通常放程序的主体或者需要不断刷新的语句
* @param 无
* @retval 无
*/
void loop()
{
	if (deviceConnected) /* 设备已经连接上 */
	{
		lcd_show_string(30,110,290,16,LCD_FONT_16,"client connectioned ", RED);
		pTxCharacteristic->setValue(&txValue, 1); /* 设置要发送的值为 1 */
		lcd_show_string(30, 130, 200, 16, LCD_FONT_16, "notify value:", RED);
		pTxCharacteristic->notify(); /* 广播 txValue++ */
		lcd_show_num(134, 130, txValue, 3, LCD_FONT_16, BLUE);
		txValue++;
		delay(10); /* 如果发送的数据包太多,蓝牙堆栈将进入拥塞状态 */
	}
	if (!deviceConnected && oldDeviceConnected) /* 断开连接 */
	{
		lcd_fill(0, 110, 319, 239, WHITE); /* 清空显示区域 */
		delay(500); /* 蓝牙堆栈有机会做好准备 */
		pServer->startAdvertising(); /* 重启广播 */
		lcd_show_string(30,110,290,16,LCD_FONT_16,"start advertising... ", RED);
		oldDeviceConnected = deviceConnected;
	}
	if (deviceConnected && !oldDeviceConnected) /* 正在连接 */
	{
		oldDeviceConnected = deviceConnected;
	}
}

编写回调函数 onConnect、 onDisconnect 和 onWrite,在 setup 函数中会进行设置。
在 setup 函数中,调用 led_init 函数完成 LED 初始化,调用 uart_init 函数完成串口初始化,调用 xl9555_init 函数完成 XL9555 初始化,调用 lcd_init 函数完成 LCD 屏初始化,调用BLEDevice::init 函数创建一个 BLE 设备,调用 BLEDevice::createServer 创建一个 BLE 服务,调用 createService 创建蓝牙服务器, 调用 createCharacteristic 函数创建 2 个 BLE 特征,然后调用start 函数启动设备,最后调用 getAdvertising 的 start 函数开始广播。
在 loop 函数中, 通过 deviceConnected 变量执行不同的操作,设备已经连接上就往 TX 特征值中发送数据,断开连接就重新尝试链接。 发送数据主要通过 setValue 和 notify 函数实现,而接收数据主要通过 getValue 函数实现。

4. 下载验证

程序下载成功后,我们可以看到 LCD 显示 ESP32-S3 蓝牙等待被连接, 如下图所示:
在这里插入图片描述
通过手机蓝牙助手“BLE 调试助手” 连接上后, LCD 显示,如下图所示:
在这里插入图片描述
手机蓝牙助手,如下图所示:
在这里插入图片描述
可以看到上图左边部分,显示了蓝牙的名称“ UART Service”,创建了一个 UUID 为“6E400001-B5A3-F393-E0A9-E50E24DCCA9E”的服务。在该服务下,创建了两个特征,发送特征的 UUID 为“ 6E400003-B5A3-F393-E0A9-E50E24DCCA9E”,接收特征的 UUID 为“6E400002-B5A3-F393-E0A9-E50E24DCCA9E”。服务器的发送功能,对于客户端来说,就需要进行接收,所以这里发送特征的属性权限为 NOTIFY。服务器的接收功能,对于客户端来说,就是需要进行发送,所以这里接收特征的属性权限为 WRITE。
点击上图中左边部分的“向下箭头图标”进入到接收数据界面即上图中中间部分,可以看到数据被传送过来。点击上图中左边部分的“向上箭头图标”进入到发送数据界面即上图中右边部分,在文本输入框中输入“ALIENTEK ESP32S3”,点击发送,最终效果如图 32.4.2 所示,接收到数据。
BLE 涉及到比较多的知识点,需要大家自行去学习。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐