Arduino 入门学习笔记(二十九):BLE_SCAN 实验

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

1. 蓝牙基础知识和 BLEScan 介绍

1.1 蓝牙介绍

蓝牙, 是一种支持设备短距离通信的无线通信技术,最早是由爱立信公司于 1994 年发明。蓝牙的目标是使各类移动设备、嵌入式设备、计算机外设和家用电器等众多设备之间在没有电缆连接的情况下能够在短距离范围内实现信息的自由传输与分享。相比较其他无线通信技术,蓝牙具有安全性高、易于连接等优势。
蓝牙采用分散式网络结构以及快跳频和短包技术,支持点对点及点对多点的通信,工作在全球通用的 2.4 GHz ISM(即工业、科学、医学)频段。蓝牙可分为经典蓝牙和低功耗蓝牙。
(1)经典蓝牙。经典蓝牙,简称 BT, 泛指支持蓝牙协议在 4.0 版本以下的模块,一般用于如语音、音乐等大数据量的传输。经典蓝牙的协议包含了个人局域网的各种规范,不同的规范对应于不同的应用场景,比较常用的有:适用于音频的 Advance Audio Distribution Profile、适用于免提设备的 Hands-Free Profile/Head-Set Profile(HFP/HSP)、适用于文本串口透传的 SerialPort Profile(SPP)、适用于无线输入/输出设备的 Human Interface Device(HID)。
(2)低功耗蓝牙。低功耗蓝牙,简称 BLE, 是一种新型的超低功耗无线通信技术,主要针对低成本、低复杂度的无线体域网和无线个域网设计,最主要的优点之一是可以用纽扣电池为低功耗蓝牙芯片供电,结合微型传感器构建出各种嵌入式传感器或可穿戴式传感器与传感器网络应用。
总体来说,蓝牙协议版本有两个分支,分别是经典蓝牙和低功耗蓝牙。其中,蓝牙 1.1、 1.2、2.0、 2.1、 3.0 版本属于经典蓝牙, 4.0 版本的蓝牙包括经典蓝牙和低功耗蓝牙, 4.0 版本以后的蓝牙添加了低功耗蓝牙。

1.2 蓝牙协议介绍

蓝牙协议规定了两个层次,分别为蓝牙核心协议和蓝牙应用层协议。蓝牙核心协议是对蓝牙技术本身的规范,主要包括控制器(Controller)和主机(Host),不涉及其应用方式;蓝牙应用层协议是在蓝牙核心协议的基础上,根据具体的应用需求定义出的特定策略。蓝牙协议栈如下图所示:
在这里插入图片描述
低功耗蓝牙协议栈包含物理层(Physical Layer, PHY)、链路层(Link Layer, LL)、主机控制接口(Host Controller Interface, HCI)、逻辑链路控制和适配协议(Logical Link Control andAdaptation Protocol, L2CAP)、属性协议(Attribute Protocol, ATT)、安全服务规范(SecurityManage Protocol, SMP)、通用属性协议(Generic Attribute Profile, GATT)、通用接入规范(GenericAccess Profile, GAP)。
物理层 PHY 工作在 2.4GHz ISM 无线频段,次啊用高斯频移键控 GFSK 的调制方式,负责从物理信道发送和接收数据包。在低功耗蓝牙中, 2.4GHz ISM 频段被划分为 40 个信道,单个信道宽度为 2MHz,物理层速率为 1Mb/s。在这 40 个信道中, 0-36 号信道采用自适应调频技术收发数据, 37-39 号信道负责广播。
链路层 LL 位于物理层之上,为逻辑链路控制和适配协议提供服务,负责广播、扫描、建立和维护链接,选择正确的方式、合适的信道交换数据包,并支持不同的拓朴结构,其操作可描述为就绪、初始、扫描、链接、广播 5 种状态,是整个低功耗蓝牙协议栈的核心。
主机控制接口 HCI 提供了主机与控制器通信的通信方式和命令时间格式。它允许主机将命令和数据发送到控制器,并允许控制器将时间和数据发送到主机。主机控制接口由两部分组成,逻辑接口和物理接口。逻辑接口定义了命令和事件以及相关行为。物理接口定义了命令、事件和数据如何通过不同的链接技术来传输。
逻辑链路控制和适配协议 L2CAP 向上层协议(协议复用、分段、重组操作)提供连接导向和无连接的数据服务,并按通道进行流量控制和重传。
属性协议 ATT 允许蓝牙设备以“属性”(Attribute)的形式向其他蓝牙设备暴露自己的某些数据。通过一个固定的 L2CAP 信道, ATT 客户端与位于远端设备上的 ATT 服务器进行交互。在 ATT 协议种,暴露属性的称为服务器 Server,而另一端就是客户端 Client。
安全服务规范 SMP 负责管理低功耗蓝牙连接的加密和安全,既保证连接的安全性,同时又不影响用户的体验。 安全服务规范通过一个固定的 L2CAP 信道,实现设备间的配对、认证和加密功能。
通用属性协议 GATT 位于属性协议之前,定义了属性的类型及其使用方法。通用属性协议GATT 和属性协议 ATT 被强制安装在低功耗蓝牙上用于支持发现远端设备服务的功能。
通用接入规范 GAP 定义了蓝牙设备如何发现、链接及绑定其他设备,这是其他蓝牙应用得以运用的基础。低功耗蓝牙定义 4 种 GAP 角色:广播设备、观察设备、中心设备和外围设备。中心设备时向外围设备发起链接的设备,需同时具有发射和接收装置,一旦链接成功,它将称为主设备,被链接的外围设备将称为从设备

1.3 工作状态和工作角色介绍

链路层定义了蓝牙的五种状态:就绪态、广播态、扫描态、发起态和链接态,如下图所示:
在这里插入图片描述
处于就绪态(Standby)时,链路层不收发报文,任何状态都可以进入到就绪态。
处于广播态(Advertising)的链路层可以发送广播报文,也可以监听以及响应这些广播报文触发的响应报文。可被发现(也就是可以被扫描到)或可被链接(后面进行数据通信)的设备必须处于广播态,而需要向一定范围内的其他蓝牙设备广播数据的设备也必须处于广播态。
处于扫描态(Scanning)的设备能够接收广播报文。扫描又分为两种,主动扫描和被动扫描。主动扫描可以发送扫描请求给广播态设备,并获取额外的扫描响应数据,而被动扫描仅仅是接收到广播报文。
处于发起态(Initiating)设备可以发起链接请求。如果处于发起态的发起者接收到了来自其他设备的广播报文,链路层会向其发送链接请求并进入链接态。如果发起者不再发起链接,也可进入到就绪态。
处于链接态(Connection)下的设备才是我们经常看到的数据传输状态。并且又会区分为主设备(Master)和从设备(Slave)。 由发起态进入链接态的设备是主设备,由广播态进入链接态的设备是从设备。
根据链路层处于状态不同,可分为 5 种不同的工作角色:广播者、扫描者、发起者、主设备和从设备。
链路层不能同时执行主设备和从设备两个角色。执行主设备角色的链路层可以同时执行广播者角色,或者扫描者角色,或者发起者角色。执行从设备角色的链路层可以同时执行广播者角色或扫描者角色。从设备不能发送可链接的广播报文,但可以发送不可链接的广播报文或可发现的广播报文。

1.4 蓝牙设备链接建立过程

传统蓝牙支持两种拓扑结构,微微网和分布式网络,而低功耗蓝牙仅支持微微网拓扑结构。对于低功耗蓝牙来说,一个主设备是可以跟多个从设备进行通信的,拓扑结构如下图所示:
在这里插入图片描述
接下来讲解一下,两个蓝牙设备是如何建立链接并实现通信的。假设有两个蓝牙设备,即一台手机 A 和照相机 B 要进行链接并实现通信,如下图所示:
在这里插入图片描述
这个链接过程如下:
(1)设备 B 要处在“可见”或“可被发现”模式下。在这种模式下,设备 B 就可被设备A 或其他蓝牙设备所发现,即“可被发现”状态。
(2)设备 A 在一定的距离内,通过在广播信道上发送广播包搜寻可被发现的蓝牙设备,这个过程称为“询问”。在“询问”的过程中,设备 A 将定位设备 B。
(3)定位设备 B 后,设备 A 将建立与设备 B 的链接。此时,设备 B 变为“可被链接”状态。
(4)一切就绪后,设备 A 建立一个与设备 B 之间的链接,这个过程称为“扫描”。
(5)两个设备之间的链接一旦建立,最先发起链接请求的设备 A 称为主设备,被建立链接的设备 B 就是从设备。此时,主设备和从设备之间的通信已建立,并可以在同步同频的状态下相互收发数据包。
(6)当主、从设备不再需要连接时,主、从设备都可以发起断开链接请求,并断开两个设备间的链接。

1.5 蓝牙扫描介绍

蓝牙设备想要组建网络传输数据,首先得通过广播或扫描发现周围设备,其次才是创建连接,前者为本实验要实现的内容:蓝牙扫描。 蓝牙扫描只是发现周边的蓝牙设备,并没有与任一进行连。
从机想要被主机连接,那么它就必须要先被主机发现。这时候,从机把自身消息以广播形式发送出去。从机需要先进行广播,不断发送广播包,没发送一次广播包,我们称之为一次广播事件。当进行广播事件时,每一个事件包含 3 个广播包,分别在 37、 38 和 39 这三个信道上同时广播相同的消息。
扫描是一个在一定范围内用来寻址其他低功耗蓝牙设备广播的过程,扫描者在扫描过程中会使用广播信道。与广播过程不同的是,扫描过程没有严格的事件定义和信道规则,按照主机设定的扫描定时参数进行。
蓝牙扫描被分为被动扫描和主动扫描。
被动扫描。在被动扫描中,扫描者仅仅监听广播包,而不向广播者发送任何数据,被动扫描的过程如下:
在这里插入图片描述
一旦设置好扫描参数,主机就可以在协议栈中发送命令启动扫描。在扫描过程中,如果控制器接收到符合过滤策略或其他规则的广播包,则向主机发送一个报告事件。报告事件除了包括广播者的设备地址,还包括广播包中的数据,以及接收广播包时的信号接收强度。利用信号接收强度以及广播包中的发射功率,共同确定信号的路径损失,从而给出大致的范围,该方面的典型应用就是防丢器和蓝牙定位。
主动扫描。在主动扫描中,主机不仅可以捕获到从机发送的广播包,还可以捕获扫描响应包,并区分广播包和扫描相应包。主动扫描的过程如下图所示:
在这里插入图片描述
控制器收到数据后将向主机发送一个报告事件,该报告事件包括了链路层数据包的广播类型,因此主机能够判断从机是否可以连接或扫描,并区分广播包和扫描响应包。
蓝牙扫描中获取到的蓝牙设备数据是来自于 ADV_IND 包。 ADV_IND 包中包含广播者设备地址、设备名、 发送功率和信号强度等。

1.6 BLEScan 库函数介绍

ESP32-S3 集成了低功耗蓝牙系统,支持 Bluetooth 5 和 bluetooth Mesh,不支持经典蓝牙。而 ESP32 是同时支持 BT 和 BLE 的。
本实验实现的 BLE 网络扫描主要依赖的是 BLEScan 库,还会涉及到 BLEDevice 库。BLEDevice 库主要用到 init 函数和 getScan 函数。
ESP32-S3 BLESCAN 功能函数主要分为两类:管理扫描和配置扫描,如下图所示:
在这里插入图片描述
管理扫描主要是管理扫描整个扫描过程,包括启动、获取结果、停止等;而配置扫描主要是配置如何进行扫描。
本实验介绍到的函数可在以下文件中找到:
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\BLEScan.cpp
接下来,我们介绍一下本实验所用到的用到的函数。
第一个函数: init 函数,该函数功能是创建一个 BLE 设备。

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

参数 deviceName 为 BLE 设备名称。
无返回值。
第二个函数: getScan 函数,该函数功能是创建扫描对象。

BLEScan* BLEDevice::getScan()

无参数;
返回值为 BLEScan 对象。
第三个函数: start 函数,该函数功能是启动同步扫描。

BLEScanResults BLEScan::start(uint32_t duration, bool is_continue)

参数 duration 为扫描时间;
参数 is_continue 为是否清楚扫描设备映射, false 表示清除;
返回值为 BLEScanResults 对象。
第四个函数: getResults 函数, 该函数的功能是获取扫描结果。

BLEScanResults BLEScan::getResults()

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

void BLEScan::clearResults()

无参数;
无返回值。

2. 硬件设计

2.1 例程功能

程序下载完成, ESP32-S3 尝试扫描附近的蓝牙设备,并把扫描到的设备名字以及信号强度显示出来。

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 程序解析

04_ble_scan.ino 代码
在 04_ble_sacn.ino 里面编写如下代码:

#include "led.h"
#include "uart.h"
#include "xl9555.h"
#include "spilcd.h"
#include <BLEDevice.h> /* 蓝牙 BLE 设备库 */
#include <BLEUtils.h>
#include <BLEScan.h> /* 蓝牙 BLE 设备的扫描功能库 */
#include <BLEAdvertisedDevice.h> /* 扫描到的蓝牙设备(广播状态) */
int scanTime = 5; /* 蓝牙扫描时间 */
BLEScan* pBLEScan; /* 扫描对象 */
char ble_addr[100]; /* 存放 ble 地址 */
uint8_t show_index = 0;
/* BLE 广播回调函数(每次扫描到广播设备时被调用) */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks
{
void onResult(BLEAdvertisedDevice advertisedDevice)
{
lcd_show_string(30, 110, 200, 16, LCD_FONT_16, "BLE_DEV_Name", BLUE);
lcd_show_string(238, 110, 200, 16, LCD_FONT_16, "RSSI", BLUE);
if (advertisedDevice.haveName())
{
lcd_fill(30,130+ show_index * 20, 319, 148 + show_index * 20, WHITE);
lcd_show_string(30, 130 + show_index * 20, 300, 16, LCD_FONT_16,
(char *)advertisedDevice.getName().c_str(), BLUE); /* 显示 BLE 设备名字 */
if (advertisedDevice.haveRSSI())
{
char ble_rssi[20]; /* 存放 ble 强度 */
sprintf(ble_rssi, "%4d", advertisedDevice.getRSSI());
lcd_show_string(238, 130 + show_index * 20, 200, 16, LCD_FONT_16,
ble_rssi, RED); /* 显示 BLE 设备强度 */
}
show_index++;
if (show_index == 4) show_index = 0; /* 屏幕只显示 4 个 BLE 设备 */
}
}
};
/**
* @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 SCAN TEST", RED);
lcd_show_string(30, 90, 200, 16, LCD_FONT_16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, LCD_FONT_16, "Scanning...", RED);
BLEDevice::init("ESP BLEDevice"); /* 创建一个 BLE 设备 */
pBLEScan = BLEDevice::getScan(); /* 创建新的扫描 */
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
/* 初始化回调函数 */
pBLEScan->setActiveScan(true); /* 主动扫描消耗更多的能量,但更快地得到结果 */
pBLEScan->setInterval(100); /* 设置扫描间隔 */
pBLEScan->setWindow(99); /* 设置窗口大小 */
}
/**
* @brief 循环函数,通常放程序的主体或者需要不断刷新的语句
* @param 无
* @retval 无
*/
void loop()
{
BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
/* 启动 BLE 扫描,并在扫描到广播设备时调用回调函数 */
lcd_show_string(120, 210, 200, 16, LCD_FONT_16, "Devices found: ", RED);
lcd_show_num(240, 210, foundDevices.getCount(), 2, LCD_FONT_16, RED);
lcd_show_string(30, 210, 200, 16, LCD_FONT_16, "Scan done!", RED);
pBLEScan->clearResults(); /* 从 BLEScan 缓冲区中删除结果以释放内存 */
delay(2000);
}

首先定义 BLEScan 对象实例为 pBLEScan。 编写回调函数 onResult,在 setup 函数中会进行设置。
在 setup 函数中,调用 led_init 函数完成 LED 初始化,调用 key_init 函数完成 KEY 初始化,调用 uart_init 函数完成串口初始化,调用 xl9555_init 函数完成 XL9555 初始化,调用 lcd_init 函数完成 LCD 屏初始化,调用 BLEDevice::init 函数创建 BLE 设备,然后通过 BLEDevice::getScan函数创建新的扫描,通过 setAdvertisedDeviceCallbacks 函数初始化回调函数,通过 setActiveScan函数设置主动扫描,通过 setInterval 函数设置扫描间隔,通过 setWindow 设置窗口大小。
在 loop 函数中,通过 start 函数启动 BLE 扫描,扫描到广播设备时会进入到回调函数OnResult 中进行处理,显示设备名称和信号强度,最后调用 clearResults 函数从缓冲区中删除扫描结果释放内存。

4. 下载验证

.程序下载成功后,我们可以看到 LCD 显示扫描到的 BLE 设备名称和信号强度, 如下图所示:
在这里插入图片描述

Logo

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

更多推荐