使用 Seeed Studio XIAO MG24 的蓝牙功能
本文档由 AI 翻译。如您发现内容有误或有改进建议,欢迎通过页面下方的评论区,或在以下 Issue 页面中告诉我们:https://github.com/Seeed-Studio/wiki-documents/issues
Seeed Studio XIAO MG24 是一款支持 Bluetooth LE 5.3 和蓝牙 Mesh 的强大开发板,非常适合需要无线连接的各种物联网应用。凭借其卓越的射频性能,XIAO MG24 提供可靠的高速无线通信,适用于短距离和长距离的多种应用场景。在本教程中,我们将探索 XIAO MG24 蓝牙功能的基本特性,包括如何扫描附近的蓝牙设备、建立蓝牙连接,以及通过该连接发送和接收数据。
切换天线的方法
Seeed Studio XIAO MG24 提供两种天线选项:内置天线和外置天线。为了方便起见,您可以选择使用内置天线;如果需要增强信号强度,可以选择外置天线。以下是切换两种天线的方法。
PB04 用于选择使用内置天线还是外置天线。在此之前,您需要将 PB05 设置为高电平以启用此功能。如果 PB04 设置为低电平,则使用内置天线;如果设置为高电平,则使用外置天线。默认设置为低电平。如果您想将其设置为高电平,可以参考以下代码。
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4
void setup() {
// 启用天线功能
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);
delay(100);
// HIGH -> 使用外置天线 / LOW -> 使用内置天线
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, HIGH);
蓝牙低功耗 (BLE) 使用
蓝牙低功耗(简称 BLE)是蓝牙的一种节能变体。BLE 的主要应用是短距离传输少量数据(低带宽)。与始终开启的传统蓝牙不同,BLE 除非建立连接,否则始终处于休眠模式。
由于其特性,BLE 非常适合需要定期交换少量数据并运行在纽扣电池上的应用。例如,BLE 在医疗保健、健身、追踪、信标、安全和家庭自动化行业中非常有用。
这使得它的功耗非常低。根据使用场景,BLE 的功耗约为传统蓝牙的 1/100。
关于 XIAO MG24 的 BLE 部分,我们将在以下章节中介绍其使用方法。
- 一些基本概念 -- 我们将首先了解一些在 BLE 中可能经常使用的概念,以帮助我们理解 BLE 程序的执行过程和思路。
- BLE 扫描器 -- 本节将解释如何搜索附近的蓝牙设备并在串口监视器中打印出来。
- BLE 服务端/客户端 -- 本节将解释如何将 XIAO MG24 用作服务端和客户端来发送和接收指定的数据消息。它还将用于从手机向 XIAO 发送或接收消息。
一些基本概念
服务端和客户端
在蓝牙低功耗中,有两种类型的设备:服务端和客户端。XIAO MG24 可以充当客户端或服务端。
服务端广播其存在,以便其他设备可以找到它,并包含客户端可以读取的数据。客户端扫描附近的设备,当找到目标服务端时,它会建立连接并监听传入数据。这被称为点对点通信。

属性
属性实际上是一段数据。每个蓝牙设备都用于提供服务,而服务是数据的集合,这个集合可以称为数据库,数据库中的每一条记录就是一个属性(Attribute)。因此,这里可以将属性理解为数据条目。您可以将蓝牙设备想象成一张表格,表格中的每一行就是一个属性。

GATT
当两个蓝牙设备建立连接时,它们需要一个协议来确定如何通信。GATT(通用属性配置文件)就是这样一个协议,它定义了蓝牙设备之间如何传输数据。
在 GATT 协议中,设备的功能和属性被组织成称为服务(services)、特性(characteristics)和描述符(descriptors)的结构。服务表示设备提供的一组相关功能和特性。每个服务可以包含多个特性,这些特性定义了服务的某些属性或行为,例如传感器数据或控制命令。每个特性都有一个唯一标识符和一个值,可以通过读取或写入该值进行通信。描述符用于描述特性的元数据,例如特性值的格式和访问权限。
通过使用 GATT 协议,蓝牙设备可以在不同的应用场景中进行通信,例如传输传感器数据或控制远程设备。
BLE 特性
ATT,全称属性协议(Attribute Protocol)。最终,ATT 是由一组 ATT 命令组成的,即请求和响应命令。ATT 也是蓝牙空包的最上层,即 ATT 是我们分析蓝牙数据包时最常接触的部分。
ATT 命令正式名称为 ATT PDU(协议数据单元,Protocol Data Unit)。它包括四种类别:读取(read)、写入(write)、通知(notify)和指示(indicate)。这些命令可以分为两种类型:如果需要响应,则会跟随一个请求;相反,如果只需要一个 ACK 而不需要响应,则不会跟随请求。
服务(Service)和特性(Characteristic)是在 GATT 层中定义的。服务端提供服务,服务是数据,而数据是属性。服务和特性是数据的逻辑表示,或者说用户可以看到的数据最终会转化为服务和特性。
让我们从移动端的角度看看服务和特性是什么样子。nRF Connect 是一个应用程序,它可以非常直观地展示每个数据包的样子。

如您所见,在蓝牙规范中,每个特定的蓝牙应用程序由多个服务组成,每个服务由多个特性组成。一个特性由 UUID、属性(Properties)和值(Value)组成。

属性用于描述对特性进行操作的类型和权限,例如是否支持读取、写入、通知等。这类似于 ATT PDU 中包含的四种类别。

UUID
每个服务、特性和描述符都有一个 UUID(通用唯一标识符,Universally Unique Identifier)。UUID 是一个唯一的 128 位(16 字节)数字。例如:
ea094cbd-3695-4205-b32d-70c1dea93c35
对于所有类型、服务和由 SIG(蓝牙特别兴趣小组) 指定的配置文件,都有缩短的 UUID。但如果您的应用程序需要自己的 UUID,可以使用此 UUID 生成器网站 来生成。
BLE 扫描器
创建一个 XIAO MG24 BLE 扫描器非常简单。以下是一个创建扫描器的示例程序。
/*
BLE 扫描示例
此示例扫描其他 BLE 设备,并打印出每个发现设备的地址、RSSI、频道和名称。
有关 Silabs BLE API 使用的更多信息,请访问:https://docs.silabs.com/bluetooth/latest/bluetooth-stack-api/
此示例仅适用于 'BLE (Silabs)' 协议栈变体。
兼容的开发板:
- Arduino Nano Matter
- SparkFun Thing Plus MGM240P
- xG27 DevKit
- xG24 Explorer Kit
- xG24 Dev Kit
- BGM220 Explorer Kit
- Ezurio Lyra 24P 20dBm Dev Kit
- Seeed Studio XIAO MG24 (Sense)
作者: Tamas Jozsi (Silicon Labs)
*/
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4
void setup() {
Serial.begin(115200);
}
void loop() {
}
static String get_complete_local_name_from_ble_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t* response);
/**************************************************************************/ /**
* 蓝牙栈事件处理器
* 当 BLE 栈上发生事件时调用
*
* @param[in] evt 来自蓝牙栈的事件
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t* evt) {
static uint32_t scan_report_num = 0u;
sl_status_t sc;
switch (SL_BT_MSG_ID(evt->header)) {
// 当 BLE 设备成功启动时接收到此事件
case sl_bt_evt_system_boot_id:
// 打印欢迎信息
Serial.begin(115200);
// 打开天线功能
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);
delay(100);
// HIGH -> 使用外部天线 / LOW -> 使用内置天线
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, HIGH);
Serial.println();
Serial.println("Silicon Labs BLE 扫描示例");
Serial.println("BLE 栈已启动");
// 开始扫描其他 BLE 设备
sc = sl_bt_scanner_set_parameters(sl_bt_scanner_scan_mode_active, // 模式
16, // 间隔(值 * 0.625 毫秒)
16); // 窗口(值 * 0.625 毫秒)
app_assert_status(sc);
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("开始扫描...");
break;
// 当扫描到其他 BLE 设备的广告时接收到此事件
case sl_bt_evt_scanner_legacy_advertisement_report_id:
scan_report_num++;
Serial.print(" -> #");
Serial.print(scan_report_num);
Serial.print(" | 地址: ");
for (int i = 5; i >= 0; i--) {
Serial.printf("%02x", evt->data.evt_scanner_legacy_advertisement_report.address.addr[i]);
if (i > 0) {
Serial.print(":");
}
}
Serial.print(" | RSSI: ");
Serial.print(evt->data.evt_scanner_legacy_advertisement_report.rssi);
Serial.print(" dBm");
Serial.print(" | 频道: ");
Serial.print(evt->data.evt_scanner_legacy_advertisement_report.channel);
Serial.print(" | 名称: ");
Serial.println(get_complete_local_name_from_ble_advertisement(&(evt->data.evt_scanner_legacy_advertisement_report)));
break;
// 默认事件处理器
default:
Serial.print("BLE 事件: 0x");
Serial.println(SL_BT_MSG_ID(evt->header), HEX);
break;
}
}
/**************************************************************************/ /**
* 在 BLE 广告中查找完整的本地名称
*
* @param[in] response 从扫描中接收到的 BLE 响应事件
*
* @return 如果找到完整的本地名称则返回,否则返回 "N/A"
*****************************************************************************/
static String get_complete_local_name_from_ble_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t* response) {
int i = 0;
// 遍历响应数据
while (i < (response->data.len - 1)) {
uint8_t advertisement_length = response->data.data[i];
uint8_t advertisement_type = response->data.data[i + 1];
// 如果长度超过设备名称的最大可能长度
if (advertisement_length > 29) {
continue;
}
// 类型 0x09 = 完整的本地名称,0x08 = 缩短的名称
// 如果字段类型与完整的本地名称匹配
if (advertisement_type == 0x09) {
// 复制设备名称
char device_name[advertisement_length + 1];
memcpy(device_name, response->data.data + i + 2, advertisement_length);
device_name[advertisement_length] = '\0';
return String(device_name);
}
// 跳到下一个广告记录
i = i + advertisement_length + 1;
}
return "N/A";
}
#ifndef BLE_STACK_SILABS
#error "此示例仅与 Silicon Labs BLE 栈兼容。请在 'Tools > Protocol stack' 中选择 'BLE (Silabs)'。"
#endif
:::提示 需要注意的是,在编译之前,必须在“Tools > Protocol stack”中选择“BLE (Silabs)”。

现在你可以选择 XIAO MG24 主板并上传程序。如果程序运行顺利,打开串行监视器并将波特率设置为 115200,你将看到以下结果。

该程序会打印出扫描到的蓝牙设备的名称、MAC 地址、频道和信号。
程序注释
此示例演示如何使用 Silicon Labs BLE 栈扫描附近的蓝牙低功耗 (BLE) 设备,并打印每个发现设备的地址、RSSI(接收信号强度指示器)、频道和名称。
代码首先定义了一个事件处理函数 sl_bt_on_event
,用于处理由 BLE 栈生成的各种蓝牙低功耗 (BLE) 事件。此函数使用 switch 语句区分事件类型,例如当 BLE 设备启动时以及接收到附近设备的广告报告时。在接收到启动事件后,它初始化串行通信,配置 GPIO 引脚以控制天线,并以指定参数开始扫描 BLE 设备。
当扫描过程检测到来自 BLE 设备的广告报告时,会触发 sl_bt_evt_scanner_legacy_advertisement_report_id
案例。在这种情况下,函数会为每个检测到的设备递增计数器,并提取关键信息,包括设备的地址、RSSI、频道和本地名称。它利用辅助函数 get_complete_local_name_from_ble_advertisement
从广告数据中检索设备的完整名称,然后将其打印到串行输出。
辅助函数 get_complete_local_name_from_ble_advertisement
遍历广告数据以定位完整的本地名称字段。它检查每个广告记录是否包含与完整本地名称对应的类型,并将其作为字符串返回。如果未找到完整名称,函数返回“N/A”。这种系统化的方法使应用程序能够有效地发现和识别附近的 BLE 设备,并在扫描过程中提供有价值的信息。
BLE 服务器/客户端
如前所述,XIAO MG24 可以充当服务器和客户端。让我们看看作为服务器的程序以及如何使用它。在将以下程序上传到 XIAO 后,它将充当服务器并向所有连接到 XIAO 的蓝牙设备发送“Hello World”消息。
// 服务器代码
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4
bool notification_enabled = false;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
Serial.begin(115200);
Serial.println("Silicon Labs BLE 发送 Hello World 示例");
// 打开天线功能
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);
delay(100);
// HIGH -> 使用外部天线 / LOW -> 使用内置天线
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, LOW);
}
void loop() {
if (notification_enabled) {
// 每两秒发送一次通知,消息为 'hello world'
send_helloworld_notification();
}
delay(2000);
}
static void ble_initialize_gatt_db();
static void ble_start_advertising();
static const uint8_t advertised_name[] = "XIAO_MG24 Server"; // BLE 设备名称
static uint16_t gattdb_session_id;
static uint16_t generic_access_service_handle;
static uint16_t name_characteristic_handle;
static uint16_t my_service_handle;
static uint16_t led_control_characteristic_handle;
static uint16_t notify_characteristic_handle;
/**************************************************************************/ /**
* 蓝牙栈事件处理器
* 当 BLE 栈发生事件时调用
*
* @param[in] evt 来自蓝牙栈的事件
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt) {
switch (SL_BT_MSG_ID(evt->header)) {
// -------------------------------
// 此事件表示设备已启动,且无线电已准备好。
// 在接收到此启动事件之前,请勿调用任何栈命令!
case sl_bt_evt_system_boot_id:
{
Serial.println("BLE 栈已启动");
// 初始化应用程序特定的 GATT 表
ble_initialize_gatt_db();
// 开始广播
ble_start_advertising();
Serial.println("BLE 广播已启动");
}
break;
// -------------------------------
// 此事件表示已打开新连接
case sl_bt_evt_connection_opened_id:
Serial.println("BLE 连接已打开");
break;
// -------------------------------
// 此事件表示连接已关闭
case sl_bt_evt_connection_closed_id:
Serial.println("BLE 连接已关闭");
// 重新启动广播
ble_start_advertising();
Serial.println("BLE 广播已重新启动");
break;
// -------------------------------
// 此事件表示远程 GATT 客户端更改了本地 GATT 数据库中属性的值
case sl_bt_evt_gatt_server_attribute_value_id:
// 检查更改的特性是否为 LED 控制
if (led_control_characteristic_handle == evt->data.evt_gatt_server_attribute_value.attribute) {
Serial.println("收到 LED 控制特性数据");
// 检查接收到的数据长度
if (evt->data.evt_gatt_server_attribute_value.value.len == 0) {
break;
}
// 获取接收到的字节
uint8_t received_data = evt->data.evt_gatt_server_attribute_value.value.data[0];
// 根据接收到的数据打开/关闭 LED
// 如果接收到 '0' - 关闭 LED
// 如果接收到 '1' - 打开 LED
if (received_data == 0x00) {
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
Serial.println("LED 关闭");
} else if (received_data == 0x01) {
Serial.println("LED 打开");
digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE);
}
}
break;
// -------------------------------
// 此事件在 GATT 特性状态更改时接收
case sl_bt_evt_gatt_server_characteristic_status_id:
// 如果 'Notify' 特性已更改
if (evt->data.evt_gatt_server_characteristic_status.characteristic == notify_characteristic_handle) {
// 客户端刚刚启用了通知 - 发送当前状态的通知
if (evt->data.evt_gatt_server_characteristic_status.client_config_flags & sl_bt_gatt_notification) {
Serial.println("通知已启用");
notification_enabled = true;
} else {
Serial.println("通知已禁用");
notification_enabled = false;
}
}
break;
// -------------------------------
// 默认事件处理器
default:
break;
}
}
/**************************************************************************/ /**
* 如果启用了通知,则向客户端发送 BLE 通知
*****************************************************************************/
static void send_helloworld_notification() {
uint8_t str[12] = "Hello World";
sl_status_t sc = sl_bt_gatt_server_notify_all(notify_characteristic_handle,
sizeof(str),
(const uint8_t *)&str);
if (sc == SL_STATUS_OK) {
Serial.println("发送通知!");
}
}
/**************************************************************************/ /**
* 启动 BLE 广播
* 如果首次调用,则初始化广播
*****************************************************************************/
static void ble_start_advertising() {
static uint8_t advertising_set_handle = 0xff;
static bool init = true;
sl_status_t sc;
if (init) {
// 创建一个广播集
sc = sl_bt_advertiser_create_set(&advertising_set_handle);
app_assert_status(sc);
// 将广播间隔设置为 100ms
sc = sl_bt_advertiser_set_timing(
advertising_set_handle,
160, // 最小广播间隔(毫秒 * 1.6)
160, // 最大广播间隔(毫秒 * 1.6)
0, // 广播持续时间
0); // 最大广播事件数
app_assert_status(sc);
init = false;
}
// 生成广播数据
sc = sl_bt_legacy_advertiser_generate_data(advertising_set_handle, sl_bt_advertiser_general_discoverable);
app_assert_status(sc);
// 开始广播并启用连接
sc = sl_bt_legacy_advertiser_start(advertising_set_handle, sl_bt_advertiser_connectable_scannable);
app_assert_status(sc);
}
/**************************************************************************/ /**
* 初始化 GATT 数据库
* 创建一个新的 GATT 会话并添加某些服务和特性
*****************************************************************************/
static void ble_initialize_gatt_db() {
sl_status_t sc;
// 创建一个新的 GATT 数据库
sc = sl_bt_gattdb_new_session(&gattdb_session_id);
app_assert_status(sc);
// 将通用访问服务添加到 GATT 数据库
const uint8_t generic_access_service_uuid[] = { 0x00, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(generic_access_service_uuid),
generic_access_service_uuid,
&generic_access_service_handle);
app_assert_status(sc);
// 将设备名称特性添加到通用访问服务
// 设备名称特性的值将被广播
const sl_bt_uuid_16_t device_name_characteristic_uuid = { .data = { 0x00, 0x2A } };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
generic_access_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ,
0x00,
0x00,
device_name_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
sizeof(advertised_name) - 1,
sizeof(advertised_name) - 1,
advertised_name,
&name_characteristic_handle);
app_assert_status(sc);
// 启动通用访问服务
sc = sl_bt_gattdb_start_service(gattdb_session_id, generic_access_service_handle);
app_assert_status(sc);
// 将我的 BLE 服务添加到 GATT 数据库
// UUID: de8a5aac-a99b-c315-0c80-60d4cbb51224
const uuid_128 my_service_uuid = {
.data = { 0x24, 0x12, 0xb5, 0xcb, 0xd4, 0x60, 0x80, 0x0c, 0x15, 0xc3, 0x9b, 0xa9, 0xac, 0x5a, 0x8a, 0xde }
};
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(my_service_uuid),
my_service_uuid.data,
&my_service_handle);
app_assert_status(sc);
// 将 'LED 控制' 特性添加到 Blinky 服务
// UUID: 5b026510-4088-c297-46d8-be6c736a087a
const uuid_128 led_control_characteristic_uuid = {
.data = { 0x7a, 0x08, 0x6a, 0x73, 0x6c, 0xbe, 0xd8, 0x46, 0x97, 0xc2, 0x88, 0x40, 0x10, 0x65, 0x02, 0x5b }
};
uint8_t led_char_init_value = 0;
sc = sl_bt_gattdb_add_uuid128_characteristic(gattdb_session_id,
my_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ | SL_BT_GATTDB_CHARACTERISTIC_WRITE,
0x00,
0x00,
led_control_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
1, // 最大长度
sizeof(led_char_init_value), // 初始值长度
&led_char_init_value, // 初始值
&led_control_characteristic_handle);
// 启动 Blinky 服务
sc = sl_bt_gattdb_start_service(gattdb_session_id, my_service_handle);
app_assert_status(sc);
// 将 'Notify' 特性添加到我的 BLE 服务
// UUID: 61a885a4-41c3-60d0-9a53-6d652a70d29c
const uuid_128 btn_report_characteristic_uuid = {
.data = { 0x9c, 0xd2, 0x70, 0x2a, 0x65, 0x6d, 0x53, 0x9a, 0xd0, 0x60, 0xc3, 0x41, 0xa4, 0x85, 0xa8, 0x61 }
};
uint8_t notify_char_init_value = 0;
sc = sl_bt_gattdb_add_uuid128_characteristic(gattdb_session_id,
my_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ | SL_BT_GATTDB_CHARACTERISTIC_NOTIFY,
0x00,
0x00,
btn_report_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
1, // 最大长度
sizeof(notify_char_init_value), // 初始值长度
¬ify_char_init_value, // 初始值
¬ify_characteristic_handle);
// 启动我的 BLE 服务
sc = sl_bt_gattdb_start_service(gattdb_session_id, my_service_handle);
app_assert_status(sc);
// 提交 GATT 数据库更改
sc = sl_bt_gattdb_commit(gattdb_session_id);
app_assert_status(sc);
}
#ifndef BLE_STACK_SILABS
#error "此示例仅兼容 Silicon Labs BLE 栈。请在 'Tools > Protocol stack' 中选择 'BLE (Silabs)'。"
#endif
同时,您可以在主要的移动应用商店中搜索并下载 nRF Connect 应用程序,该应用程序允许您的手机搜索并连接蓝牙设备。
- 安卓: nRF Connect
- IOS: nRF Connect
下载软件后,请按照以下步骤搜索并连接 XIAO_MG24,您将看到广播的 "Hello World"。
![]() | ![]() | ![]() | ![]() |
如果您希望使用另一个 XIAO MG24 作为客户端来接收来自服务器的消息,那么您可以为客户端 XIAO 使用以下过程。
// 客户端代码
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4
// 连接状态
enum conn_state_t {
ST_BOOT,
ST_SCAN,
ST_CONNECT,
ST_SERVICE_DISCOVER,
ST_CHAR_DISCOVER,
ST_READY
};
conn_state_t connection_state = ST_BOOT;
uint8_t connection_handle = __UINT8_MAX__;
uint32_t blinky_service_handle = __UINT32_MAX__;
uint16_t led_control_char_handle = __UINT16_MAX__;
bool gatt_procedure_in_progress = false;
// 如果没有内置按钮,请设置一个连接按钮的引脚
#ifndef BTN_BUILTIN
#define BTN_BUILTIN D0
#endif
void setup() {
// 将内置 LED 设置为输出
pinMode(LED_BUILTIN, OUTPUT);
// 关闭内置 LED
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
// 将内置按钮设置为输入
pinMode(BTN_BUILTIN, INPUT);
// 启动串口
Serial.begin(115200);
// 打开天线功能
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);
delay(100);
// HIGH -> 使用外部天线 / LOW -> 使用内置天线
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, LOW);
}
void loop() {
// 静态变量,用于记住按钮的前一个状态
static uint8_t btn_state_prev = LOW;
// 如果连接已完全建立且没有正在进行的 GATT 操作
if (connection_state == ST_READY && !gatt_procedure_in_progress) {
// 读取按钮的当前状态
uint8_t btn_state = digitalRead(BTN_BUILTIN);
// 如果当前状态与前一个状态不同
if (btn_state_prev != btn_state) {
// 更新前一个状态
btn_state_prev = btn_state;
// 反转状态(SL 板按钮按下时为 0,释放时为 1)
uint8_t btn_state_inv = !btn_state;
// 记录状态变化
Serial.print("发送按钮状态: ");
Serial.println(btn_state_inv);
// 通过写入其他设备的 LED 控制特性,通过 BLE 发送新状态
sl_status_t sc = sl_bt_gatt_write_characteristic_value(connection_handle, led_control_char_handle, sizeof(uint8_t), &btn_state_inv);
app_assert_status(sc);
gatt_procedure_in_progress = true;
}
}
}
// Blinky 服务
// UUID: de8a5aac-a99b-c315-0c80-60d4cbb51224
const uuid_128 blinky_service_uuid = {
.data = { 0x24, 0x12, 0xb5, 0xcb, 0xd4, 0x60, 0x80, 0x0c, 0x15, 0xc3, 0x9b, 0xa9, 0xac, 0x5a, 0x8a, 0xde }
};
// LED 控制特性
// UUID: 5b026510-4088-c297-46d8-be6c736a087a
const uuid_128 led_control_characteristic_uuid = {
.data = { 0x7a, 0x08, 0x6a, 0x73, 0x6c, 0xbe, 0xd8, 0x46, 0x97, 0xc2, 0x88, 0x40, 0x10, 0x65, 0x02, 0x5b }
};
const uint8_t advertised_name[] = "XIAO_MG24 Server";
static bool find_complete_local_name_in_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t* response);
/**************************************************************************/ /**
* 蓝牙堆栈事件处理程序
* 当 BLE 堆栈上发生事件时调用
*
* @param[in] evt 来自蓝牙堆栈的事件
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t* evt) {
static uint32_t scan_report_num = 0u;
sl_status_t sc;
switch (SL_BT_MSG_ID(evt->header)) {
// 当 BLE 设备成功启动时接收到此事件
case sl_bt_evt_system_boot_id:
// 打印欢迎消息
Serial.println();
Serial.println("Silicon Labs BLE 灯开关客户端示例");
Serial.println("BLE 堆栈已启动");
// 开始扫描其他 BLE 设备
sc = sl_bt_scanner_set_parameters(sl_bt_scanner_scan_mode_active, 16, 16);
app_assert_status(sc);
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("开始扫描...");
connection_state = ST_SCAN;
break;
// 当我们扫描到另一个 BLE 设备的广播时接收到此事件
case sl_bt_evt_scanner_legacy_advertisement_report_id:
scan_report_num++;
Serial.print(" -> #");
Serial.print(scan_report_num);
Serial.print(" | 地址: ");
for (int i = 5; i >= 0; i--) {
Serial.printf("%02x", evt->data.evt_scanner_legacy_advertisement_report.address.addr[i]);
if (i > 0) {
Serial.print(":");
}
}
Serial.print(" | RSSI: ");
Serial.print(evt->data.evt_scanner_legacy_advertisement_report.rssi);
Serial.print(" dBm");
Serial.print(" | 通道: ");
Serial.print(evt->data.evt_scanner_legacy_advertisement_report.channel);
Serial.print(" | 名称: ");
Serial.println(find_complete_local_name_in_advertisement(&(evt->data.evt_scanner_legacy_advertisement_report)));
// 如果我们找到其他设备的名称
if (find_complete_local_name_in_advertisement(&(evt->data.evt_scanner_legacy_advertisement_report))) {
Serial.println("目标设备已找到!");
Serial.print("正在连接到 ");
for (int i = 5; i >= 0; i--) {
Serial.printf("%02x", evt->data.evt_scanner_legacy_advertisement_report.address.addr[i]);
if (i > 0) {
Serial.print(":");
}
}
Serial.println(" ");
// 停止扫描
sc = sl_bt_scanner_stop();
app_assert_status(sc);
// 连接到设备
sc = sl_bt_connection_open(evt->data.evt_scanner_legacy_advertisement_report.address,
evt->data.evt_scanner_legacy_advertisement_report.address_type,
sl_bt_gap_phy_1m,
NULL);
// app_assert_status(sc);
connection_state = ST_CONNECT;
Serial.println("我们现在已连接到 BLE 服务器");
}
break;
// 当 BLE 连接已打开时接收到此事件
case sl_bt_evt_connection_opened_id:
Serial.println("连接已打开");
digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE);
connection_handle = evt->data.evt_connection_opened.connection;
// 在连接的设备上发现 Health Thermometer 服务
sc = sl_bt_gatt_discover_primary_services_by_uuid(connection_handle,
sizeof(blinky_service_uuid),
blinky_service_uuid.data);
app_assert_status(sc);
gatt_procedure_in_progress = true;
connection_state = ST_SERVICE_DISCOVER;
break;
// 当 BLE 连接已关闭时接收到此事件
case sl_bt_evt_connection_closed_id:
Serial.println("连接已关闭");
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
connection_handle = __UINT8_MAX__;
// 重新开始扫描
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("重新开始扫描...");
connection_state = ST_SCAN;
break;
// 当发现新服务时生成此事件
case sl_bt_evt_gatt_service_id:
Serial.println("GATT 服务已发现");
// 存储已发现的 Thermometer 服务的句柄
blinky_service_handle = evt->data.evt_gatt_service.service;
break;
// 当发现新特性时生成此事件
case sl_bt_evt_gatt_characteristic_id:
Serial.println("GATT 特性已发现");
// 存储已发现的 Temperature Measurement 特性的句柄
led_control_char_handle = evt->data.evt_gatt_characteristic.characteristic;
break;
// 当 GATT 操作完成时接收到此事件
case sl_bt_evt_gatt_procedure_completed_id:
Serial.println("GATT 操作已完成");
gatt_procedure_in_progress = false;
if (connection_state == ST_SERVICE_DISCOVER) {
Serial.println("GATT 服务发现已完成");
// 在连接的设备上发现 thermometer 特性
sc = sl_bt_gatt_discover_characteristics_by_uuid(evt->data.evt_gatt_procedure_completed.connection,
blinky_service_handle,
sizeof(led_control_characteristic_uuid.data),
led_control_characteristic_uuid.data);
app_assert_status(sc);
gatt_procedure_in_progress = true;
connection_state = ST_CHAR_DISCOVER;
break;
}
if (connection_state == ST_CHAR_DISCOVER) {
Serial.println("GATT 特性发现已完成");
connection_state = ST_READY;
break;
}
break;
// 默认事件处理程序
default:
Serial.print("BLE 事件: 0x");
Serial.println(SL_BT_MSG_ID(evt->header), HEX);
break;
}
}
/**************************************************************************/ /**
* 在 BLE 广播中查找配置的名称
*
* @param[in] response 从扫描接收到的 BLE 响应事件
*
* @return 如果找到则返回 true,否则返回 false
*****************************************************************************/
static bool find_complete_local_name_in_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t* response) {
int i = 0;
bool found = false;
// 遍历响应数据
while (i < (response->data.len - 1)) {
uint8_t advertisement_length = response->data.data[i];
uint8_t advertisement_type = response->data.data[i + 1];
// 类型 0x09 = 完整本地名称,0x08 = 缩短名称
// 如果字段类型匹配完整本地名称
if (advertisement_type == 0x09) {
// 检查设备名称是否匹配
if (memcmp(response->data.data + i + 2, advertised_name, strlen((const char*)advertised_name)) == 0) {
found = true;
break;
}
}
// 跳到下一个广播记录
i = i + advertisement_length + 1;
}
return found;
}
#ifndef BLE_STACK_SILABS
#error "此示例仅与 Silicon Labs BLE 堆栈兼容。请在 'Tools > Protocol stack' 中选择 'BLE (Silabs)'。"
#endif
上述程序将 XIAO 转变为一个客户端,并搜索附近的蓝牙设备。当蓝牙设备的 UUID 与您提供的 UUID 匹配时,它将连接到该设备并获取其特征值。

程序注释
让我们快速了解 BLE 服务器示例代码的工作原理。它首先导入了 BLE 功能所需的库。然后,您需要为服务和特征定义一个 UUID。
// 将我的 BLE 服务添加到 GATT 数据库
// UUID: de8a5aac-a99b-c315-0c80-60d4cbb51224
const uuid_128 my_service_uuid = {
.data = { 0x24, 0x12, 0xb5, 0xcb, 0xd4, 0x60, 0x80, 0x0c, 0x15, 0xc3, 0x9b, 0xa9, 0xac, 0x5a, 0x8a, 0xde }
};
// 将 'Notify' 特征添加到我的 BLE 服务
// UUID: 61a885a4-41c3-60d0-9a53-6d652a70d29c
const uuid_128 btn_report_characteristic_uuid = {
.data = { 0x9c, 0xd2, 0x70, 0x2a, 0x65, 0x6d, 0x53, 0x9a, 0xd0, 0x60, 0xc3, 0x41, 0xa4, 0x85, 0xa8, 0x61 }
};
您可以保留默认的 UUID,也可以访问 uuidgenerator.net 来为您的服务和特征生成随机 UUID。
接下来,您创建一个名为 “XIAO_MG24 Server” 的 BLE 设备。您可以将此名称更改为您喜欢的任何名称。在接下来的代码中,您将 BLE 设备设置为服务器。之后,您为 BLE 服务器创建一个服务,并使用之前定义的 UUID。
sl_status_t sc;
// 创建一个新的 GATT 数据库会话
sc = sl_bt_gattdb_new_session(&gattdb_session_id);
app_assert_status(sc);
// 将通用访问服务添加到 GATT 数据库
const uint8_t generic_access_service_uuid[] = { 0x00, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(generic_access_service_uuid),
generic_access_service_uuid,
&generic_access_service_handle);
app_assert_status(sc);
// 将设备名称特征添加到通用访问服务
// 设备名称特征的值将被广播
const sl_bt_uuid_16_t device_name_characteristic_uuid = { .data = { 0x00, 0x2A } };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
generic_access_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ,
0x00,
0x00,
device_name_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
sizeof(advertised_name) - 1,
sizeof(advertised_name) - 1,
advertised_name,
&name_characteristic_handle);
app_assert_status(sc);
// 启动通用访问服务
sc = sl_bt_gattdb_start_service(gattdb_session_id, generic_access_service_handle);
app_assert_status(sc);
// 将我的 BLE 服务添加到 GATT 数据库
// UUID: de8a5aac-a99b-c315-0c80-60d4cbb51224
const uuid_128 my_service_uuid = {
.data = { 0x24, 0x12, 0xb5, 0xcb, 0xd4, 0x60, 0x80, 0x0c, 0x15, 0xc3, 0x9b, 0xa9, 0xac, 0x5a, 0x8a, 0xde }
};
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(my_service_uuid),
my_service_uuid.data,
&my_service_handle);
app_assert_status(sc);
然后,您为该服务设置特征。如您所见,您还使用了之前定义的 UUID,并需要传递特征的属性作为参数。在本例中,属性为:READ 和 NOTIFY。
// 将 'Notify' 特征添加到我的 BLE 服务
// UUID: 61a885a4-41c3-60d0-9a53-6d652a70d29c
const uuid_128 btn_report_characteristic_uuid = {
.data = { 0x9c, 0xd2, 0x70, 0x2a, 0x65, 0x6d, 0x53, 0x9a, 0xd0, 0x60, 0xc3, 0x41, 0xa4, 0x85, 0xa8, 0x61 }
};
uint8_t notify_char_init_value = 0;
sc = sl_bt_gattdb_add_uuid128_characteristic(gattdb_session_id,
my_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ | SL_BT_GATTDB_CHARACTERISTIC_NOTIFY,
0x00,
0x00,
btn_report_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
1, // 最大长度
sizeof(notify_char_init_value), // 初始值长度
¬ify_char_init_value, // 初始值
¬ify_characteristic_handle);
// 启动我的 BLE 服务
sc = sl_bt_gattdb_start_service(gattdb_session_id, my_service_handle);
app_assert_status(sc);
// 提交 GATT 数据库的更改
sc = sl_bt_gattdb_commit(gattdb_session_id);
app_assert_status(sc);
创建特征后,您可以使用 sl_bt_gatt_server_notify_all()
方法设置其值。在本例中,我们将值设置为文本 “Hello World”。您可以将此文本更改为您喜欢的任何内容。在未来的项目中,此文本可以是传感器读数或灯的状态,例如。
最后,您可以启动服务和广播,以便其他 BLE 设备可以扫描并找到此 BLE 设备。
// 开始广播
ble_start_advertising();
这是一个关于如何创建 BLE 服务器的简单示例。该程序的功能是每两秒发送一次通知,内容为 "Hello World"。
BLE 传感器数据交换
接下来,我们将进入实际案例。在这个案例中,我们将使用 XIAO MG24 的 getCPUTemp()
函数来测量当前 MCU 的温度,然后通过蓝牙将 MCU 的温度值发送到另一个 XIAO MG24,以模拟健康温度计。
我们需要准备两个 XIAO,一个作为服务器,一个作为客户端。以下是作为服务器的示例程序。作为服务器的 XIAO 主要有以下任务:
- 首先,使用
getCPUTemp()
函数获取当前 MCU 的温度; - 其次,创建蓝牙服务器;
- 第三,通过蓝牙广播温度值;
- 第四,显示实时温度。
// 服务器端
/*
BLE 健康温度计示例
此示例实现了一个最小的 BLE 健康温度计配置文件,用于通过 BLE 提供温度测量数据。
启动时,程序将以配置的名称启动 BLE 广播,然后接受任何传入的连接。当设备连接并启用健康温度计特性的指示时,
设备将发送其 CPU 温度读数作为温度计数据。
使用 EFR Connect 应用程序,您可以通过进入“Demo”选项卡并选择“Health Thermometer”来测试此功能。
或者,您可以通过将另一个 BLE 板刷写为 'ble_health_thermometer_client' 示例来测试此示例,
并让两个板通过 BLE 交换温度测量数据。
有关 API 使用的更多信息,请访问:https://docs.silabs.com/bluetooth/latest/bluetooth-stack-api/
此示例仅适用于 'BLE (Silabs)' 协议栈变体。
您可以使用 EFR Connect 应用程序测试温度计设备:
- https://play.google.com/store/apps/details?id=com.siliconlabs.bledemo
- https://apps.apple.com/us/app/efr-connect-ble-mobile-app/id1030932759
兼容的开发板:
- Arduino Nano Matter
- SparkFun Thing Plus MGM240P
- xG27 DevKit
- xG24 Explorer Kit
- xG24 Dev Kit
- BGM220 Explorer Kit
- Ezurio Lyra 24P 20dBm Dev Kit
- Seeed Studio XIAO MG24 (Sense)
作者: Tamas Jozsi (Silicon Labs)
*/
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4
static void handle_temperature_indication();
static void ble_initialize_gatt_db();
static void ble_start_advertising();
const uint8_t advertised_name[] = "XIAOMG24_BLE";
uint8_t connection_handle = 0u;
uint16_t temp_measurement_characteristic_handle = 0u;
bool indication_enabled = false;
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
Serial.begin(115200);
// 启用天线功能
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);
delay(100);
// HIGH -> 使用外部天线 / LOW -> 使用内置天线
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, LOW);
}
void loop()
{
handle_temperature_indication();
}
/**************************************************************************//**
* 如果启用了指示,则通过 BLE 向连接的设备发送当前温度的指示,然后等待一秒钟
*****************************************************************************/
static void handle_temperature_indication()
{
// 如果未启用指示,则立即返回
if (!indication_enabled) {
return;
}
// 获取当前 CPU 温度
float temperature = getCPUTemp();
// 将温度转换为 IEEE 11073 浮点值
int32_t millicelsius = (int32_t)(temperature * 1000);
uint8_t buffer[5];
uint32_t tmp_value = ((uint32_t)millicelsius & 0x00ffffffu) | ((uint32_t)(-3) << 24);
buffer[0] = 0;
buffer[1] = tmp_value & 0xff;
buffer[2] = (tmp_value >> 8) & 0xff;
buffer[3] = (tmp_value >> 16) & 0xff;
buffer[4] = (tmp_value >> 24) & 0xff;
// 发送指示
sl_bt_gatt_server_send_indication(connection_handle, temp_measurement_characteristic_handle, sizeof(buffer), buffer);
// 记录温度
Serial.print("已发送温度指示 - 当前温度: ");
Serial.print(temperature);
Serial.println(" C");
// 等待一秒钟
delay(1000);
}
/**************************************************************************//**
* 蓝牙栈事件处理程序
* 当蓝牙栈上发生事件时调用
*
* @param[in] evt 来自蓝牙栈的事件
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
{
switch (SL_BT_MSG_ID(evt->header)) {
// 当 BLE 设备成功启动时接收到此事件
case sl_bt_evt_system_boot_id:
{
// 打印欢迎信息
Serial.begin(115200);
Serial.println();
Serial.println("Silicon Labs BLE 健康温度计示例");
Serial.println("BLE 栈已启动");
// 初始化应用程序特定的 GATT 数据库
ble_initialize_gatt_db();
// 开始广播
ble_start_advertising();
}
break;
// 当打开 BLE 连接时接收到此事件
case sl_bt_evt_connection_opened_id:
// 存储连接句柄,用于发送指示
connection_handle = evt->data.evt_connection_opened.connection;
Serial.println("连接已打开");
digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE);
break;
// 当关闭 BLE 连接时接收到此事件
case sl_bt_evt_connection_closed_id:
// 重置存储的值
connection_handle = 0u;
indication_enabled = false;
Serial.println("连接已关闭");
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
// 重新启动广播
ble_start_advertising();
break;
// 当 GATT 特性状态发生变化时接收到此事件
case sl_bt_evt_gatt_server_characteristic_status_id:
{
// 如果温度测量特性已更改
if (evt->data.evt_gatt_server_characteristic_status.characteristic == temp_measurement_characteristic_handle) {
uint16_t client_config_flags = evt->data.evt_gatt_server_characteristic_status.client_config_flags;
uint8_t status_flags = evt->data.evt_gatt_server_characteristic_status.status_flags;
if ((client_config_flags == 0x02) && (status_flags == 0x01)) {
// 如果客户端配置标志中启用了指示 (0x02),并且状态标志显示这是一个更改
Serial.println("温度指示已启用");
indication_enabled = true;
} else if ((client_config_flags == 0x00) && (status_flags == 0x01)) {
// 如果客户端配置标志中禁用了指示 (0x00),并且状态标志显示这是一个更改
Serial.println("温度指示已禁用");
indication_enabled = false;
}
}
}
break;
// 默认事件处理程序
default:
Serial.print("BLE 事件: 0x");
Serial.println(SL_BT_MSG_ID(evt->header), HEX);
break;
}
}
/**************************************************************************//**
* 开始 BLE 广播
* 如果是首次调用,则初始化广播
*****************************************************************************/
static void ble_start_advertising()
{
static uint8_t advertising_set_handle = 0xff;
static bool init = true;
sl_status_t sc;
if (init) {
// 创建一个广播集
sc = sl_bt_advertiser_create_set(&advertising_set_handle);
app_assert_status(sc);
// 设置广播间隔为 100ms
sc = sl_bt_advertiser_set_timing(
advertising_set_handle,
160, // 最小广播间隔(毫秒 * 1.6)
160, // 最大广播间隔(毫秒 * 1.6)
0, // 广播持续时间
0); // 最大广播事件数
app_assert_status(sc);
init = false;
}
// 生成广播数据
sc = sl_bt_legacy_advertiser_generate_data(advertising_set_handle, sl_bt_advertiser_general_discoverable);
app_assert_status(sc);
// 开始广播并启用连接
sc = sl_bt_legacy_advertiser_start(advertising_set_handle, sl_bt_advertiser_connectable_scannable);
app_assert_status(sc);
Serial.print("已开始广播,名称为 '");
Serial.print((const char*)advertised_name);
Serial.println("'...");
}
/**************************************************************************//**
* 初始化 GATT 数据库
* 创建一个新的 GATT 会话并添加某些服务和特性
*****************************************************************************/
static void ble_initialize_gatt_db()
{
sl_status_t sc;
uint16_t gattdb_session_id;
uint16_t service_handle;
uint16_t device_name_characteristic_handle;
uint16_t temp_type_characteristic_handle;
// 创建一个新的 GATT 数据库
sc = sl_bt_gattdb_new_session(&gattdb_session_id);
app_assert_status(sc);
// 通用访问服务
const uint8_t generic_access_service_uuid[] = { 0x00, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(generic_access_service_uuid),
generic_access_service_uuid,
&service_handle);
app_assert_status(sc);
// 设备名称特性
const sl_bt_uuid_16_t device_name_characteristic_uuid = { .data = { 0x00, 0x2A } };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ,
0x00,
0x00,
device_name_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
sizeof(advertised_name) - 1,
sizeof(advertised_name) - 1,
advertised_name,
&device_name_characteristic_handle);
app_assert_status(sc);
sc = sl_bt_gattdb_start_service(gattdb_session_id, service_handle);
app_assert_status(sc);
// 健康温度计服务
const uint8_t thermometer_service_uuid[] = { 0x09, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(thermometer_service_uuid),
thermometer_service_uuid,
&service_handle);
app_assert_status(sc);
// 温度测量特性
const sl_bt_uuid_16_t temp_measurement_characteristic_uuid = { .data = { 0x1C, 0x2A } };
uint8_t temp_initial_value[5] = { 0, 0, 0, 0, 0 };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
service_handle,
SL_BT_GATTDB_CHARACTERISTIC_INDICATE,
0x00,
0x00,
temp_measurement_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
5,
5,
temp_initial_value,
&temp_measurement_characteristic_handle);
app_assert_status(sc);
// 温度类型特性
const sl_bt_uuid_16_t temp_type_characteristic_uuid = { .data = { 0x1D, 0x2A } };
// 温度类型:身体 (2)
uint8_t temp_type_initial_value = 2;
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ,
0x00,
0x00,
temp_type_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
1,
1,
&temp_type_initial_value,
&temp_type_characteristic_handle);
app_assert_status(sc);
// 启动健康温度计服务
sc = sl_bt_gattdb_start_service(gattdb_session_id, service_handle);
app_assert_status(sc);
// 提交 GATT 数据库更改
sc = sl_bt_gattdb_commit(gattdb_session_id);
app_assert_status(sc);
}
#ifndef BLE_STACK_SILABS
#error "此示例仅兼容 Silicon Labs BLE 栈。请在 'Tools > Protocol stack' 中选择 'BLE (Silabs)'。"
#endif
在为其中一个 XIAO 上传程序后,如果程序运行顺利,那么您可以拿出手机并使用 nRF Connect APP 搜索名为 XIAOMG24_BLE 的蓝牙设备,连接它,并点击如下所示的按钮,您将收到温度数据的信息。

接下来,我们需要拿出另一个 XIAO,它将作为客户端来收集和显示数据。
// 客户端
/*
BLE 健康温度计客户端示例
此示例连接到运行“BLE 健康温度计”示例的另一个板,并通过 BLE 读取温度。
启动时,程序将开始扫描运行“ble_health_thermometer”示例并以“Thermometer Example”广播的其他板。
一旦找到其他板,它将建立连接,发现其服务和特性,然后订阅温度测量。
订阅后,示例将定期从其他板接收温度数据,并将其打印到串口。
有关 API 使用的更多信息,请访问:https://docs.silabs.com/bluetooth/latest/bluetooth-stack-api/
此示例仅适用于“BLE (Silabs)”协议栈变体。
兼容的开发板:
- Arduino Nano Matter
- SparkFun Thing Plus MGM240P
- xG27 DevKit
- xG24 Explorer Kit
- xG24 Dev Kit
- BGM220 Explorer Kit
- Ezurio Lyra 24P 20dBm Dev Kit
- Seeed Studio XIAO MG24 (Sense)
作者: Tamas Jozsi (Silicon Labs)
*/
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
Serial.begin(115200);
// 打开此天线功能
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);
delay(100);
// HIGH -> 使用外部天线 / LOW -> 使用内置天线
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, LOW);
}
void loop()
{
}
// 连接状态
enum conn_state_t {
ST_BOOT,
ST_SCAN,
ST_CONNECT,
ST_SERVICE_DISCOVER,
ST_CHAR_DISCOVER,
ST_REQUEST_INDICATION,
ST_RECEIVE_DATA
};
// IEEE 11073 浮点结构
typedef struct {
uint8_t mantissa_l;
uint8_t mantissa_m;
int8_t mantissa_h;
int8_t exponent;
} IEEE_11073_float;
static bool find_complete_local_name_in_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t *response);
static float translate_IEEE_11073_temperature_to_float(IEEE_11073_float const *IEEE_11073_value);
const uint8_t thermometer_service_uuid[] = { 0x09, 0x18 };
const sl_bt_uuid_16_t temp_measurement_characteristic_uuid = { .data = { 0x1C, 0x2A } };
const uint8_t advertised_name[] = "XIAOMG24_BLE";
uint32_t thermometer_service_handle = __UINT32_MAX__;
uint16_t temp_measurement_char_handle = __UINT16_MAX__;
conn_state_t connection_state = ST_BOOT;
/**************************************************************************//**
* 蓝牙栈事件处理程序
* 当 BLE 栈上发生事件时调用
*
* @param[in] evt 来自蓝牙栈的事件
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
{
sl_status_t sc;
switch (SL_BT_MSG_ID(evt->header)) {
// 当 BLE 设备成功启动时接收到此事件
case sl_bt_evt_system_boot_id:
// 打印欢迎信息
Serial.println();
Serial.println("Silicon Labs BLE 健康温度计客户端示例");
Serial.println("BLE 栈已启动");
// 开始扫描其他 BLE 设备
sc = sl_bt_scanner_set_parameters(sl_bt_scanner_scan_mode_active, 16, 16);
app_assert_status(sc);
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("开始扫描...");
connection_state = ST_SCAN;
break;
// 当扫描到其他 BLE 设备的广播时接收到此事件
case sl_bt_evt_scanner_legacy_advertisement_report_id:
Serial.println("接收到 BLE 扫描报告");
// 如果找到其他设备的名称
if (find_complete_local_name_in_advertisement(&(evt->data.evt_scanner_legacy_advertisement_report))) {
Serial.println("目标设备已找到");
// 停止扫描
sc = sl_bt_scanner_stop();
app_assert_status(sc);
// 连接到设备
sc = sl_bt_connection_open(evt->data.evt_scanner_legacy_advertisement_report.address,
evt->data.evt_scanner_legacy_advertisement_report.address_type,
sl_bt_gap_phy_1m,
NULL);
app_assert_status(sc);
connection_state = ST_CONNECT;
}
break;
// 当打开 BLE 连接时接收到此事件
case sl_bt_evt_connection_opened_id:
Serial.println("连接已打开");
digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE);
// 在连接的设备上发现健康温度计服务
sc = sl_bt_gatt_discover_primary_services_by_uuid(evt->data.evt_connection_opened.connection,
sizeof(thermometer_service_uuid),
thermometer_service_uuid);
app_assert_status(sc);
connection_state = ST_SERVICE_DISCOVER;
break;
// 当 BLE 连接关闭时接收到此事件
case sl_bt_evt_connection_closed_id:
Serial.println("连接已关闭");
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
// 重新开始扫描
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("重新开始扫描...");
connection_state = ST_SCAN;
break;
// 当发现新服务时生成此事件
case sl_bt_evt_gatt_service_id:
Serial.println("发现 GATT 服务");
// 存储发现的温度计服务的句柄
thermometer_service_handle = evt->data.evt_gatt_service.service;
break;
// 当发现新特性时生成此事件
case sl_bt_evt_gatt_characteristic_id:
Serial.println("发现 GATT 特性");
// 存储发现的温度测量特性的句柄
temp_measurement_char_handle = evt->data.evt_gatt_characteristic.characteristic;
break;
// 当 GATT 操作完成时接收到此事件
case sl_bt_evt_gatt_procedure_completed_id:
Serial.println("GATT 操作完成");
if (connection_state == ST_SERVICE_DISCOVER) {
Serial.println("GATT 服务发现完成");
// 在连接的设备上发现温度计特性
sc = sl_bt_gatt_discover_characteristics_by_uuid(evt->data.evt_gatt_procedure_completed.connection,
thermometer_service_handle,
sizeof(temp_measurement_characteristic_uuid.data),
temp_measurement_characteristic_uuid.data);
app_assert_status(sc);
connection_state = ST_CHAR_DISCOVER;
break;
}
if (connection_state == ST_CHAR_DISCOVER) {
Serial.println("GATT 特性发现完成");
// 启用温度测量指示
sc = sl_bt_gatt_set_characteristic_notification(evt->data.evt_gatt_procedure_completed.connection,
temp_measurement_char_handle,
sl_bt_gatt_indication);
app_assert_status(sc);
connection_state = ST_REQUEST_INDICATION;
break;
}
if (connection_state == ST_REQUEST_INDICATION) {
Serial.println("温度测量指示已启用");
connection_state = ST_RECEIVE_DATA;
}
break;
// 当接收到特性值(如指示)时接收到此事件
case sl_bt_evt_gatt_characteristic_value_id:
{
Serial.println("接收到 GATT 数据");
// 从事件中获取接收到的数据
uint8_t* char_value = &(evt->data.evt_gatt_characteristic_value.value.data[0]);
// 将其转换回浮点数
float temperature = translate_IEEE_11073_temperature_to_float((IEEE_11073_float *)(char_value + 1));
// 打印到串口
Serial.print("接收到的温度:");
Serial.print(temperature);
Serial.println(" C");
sc = sl_bt_gatt_send_characteristic_confirmation(evt->data.evt_gatt_characteristic_value.connection);
app_assert_status(sc);
}
break;
// 默认事件处理程序
default:
Serial.print("BLE 事件: 0x");
Serial.println(SL_BT_MSG_ID(evt->header), HEX);
break;
}
}
/**************************************************************************//**
* 在 BLE 广播中查找配置的名称
*
* @param[in] response 从扫描中接收到的 BLE 响应事件
*
* @return 如果找到返回 true,否则返回 false
*****************************************************************************/
static bool find_complete_local_name_in_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t *response)
{
int i = 0;
bool found = false;
// 遍历响应数据
while (i < (response->data.len - 1)) {
uint8_t advertisement_length = response->data.data[i];
uint8_t advertisement_type = response->data.data[i + 1];
// 类型 0x09 = 完整本地名称, 0x08 = 缩短名称
// 如果字段类型匹配完整本地名称
if (advertisement_type == 0x09) {
// 检查设备名称是否匹配
if (memcmp(response->data.data + i + 2, advertised_name, strlen((const char*)advertised_name)) == 0) {
found = true;
break;
}
}
// 跳到下一个广播记录
i = i + advertisement_length + 1;
}
return found;
}
/**************************************************************************//**
* 将 IEEE-11073 温度值转换为浮点数
*
* @param[in] IEEE_11073_value 要转换的 IEEE 11073 浮点值
*
* @return 转换后的浮点值,失败时返回 NAN
*****************************************************************************/
static float translate_IEEE_11073_temperature_to_float(IEEE_11073_float const *IEEE_11073_value)
{
int32_t mantissa = 0;
uint8_t mantissa_l;
uint8_t mantissa_m;
int8_t mantissa_h;
int8_t exponent;
// 错误参数:传递了 NULL 指针
if (!IEEE_11073_value) {
return NAN;
}
// 缓存字段
mantissa_l = IEEE_11073_value->mantissa_l;
mantissa_m = IEEE_11073_value->mantissa_m;
mantissa_h = IEEE_11073_value->mantissa_h;
exponent = IEEE_11073_value->exponent;
// IEEE-11073 标准 NaN 值
if ((mantissa_l == 0xFF) && (mantissa_m == 0xFF) && (mantissa_h == 0x7F) && (exponent == 0x00)) {
return NAN;
}
// 将 24 位有符号值转换为 32 位有符号值
mantissa |= mantissa_h;
mantissa <<= 8;
mantissa |= mantissa_m;
mantissa <<= 8;
mantissa |= mantissa_l;
mantissa <<= 8;
mantissa >>= 8;
return ((float)mantissa) * pow(10.0f, (float)exponent);
}
#ifndef BLE_STACK_SILABS
#error "此示例仅兼容 Silicon Labs BLE 栈。请在 'Tools > Protocol stack' 中选择 'BLE (Silabs)'。"
#endif
最后,如果服务器和客户端程序运行顺利,您可以通过串口看到客户端打印以下信息。

程序注释
对于上述程序,我们将挑选一些重要部分进行解释。我们从服务器程序开始。
在程序的开头,我们定义了蓝牙服务器的名称,这个名称可以是您设置的名称,但需要记住它,因为您需要依赖这个名称来搜索该蓝牙设备。
const uint8_t advertised_name[] = "XIAOMG24_BLE";
在教程的前面部分,我们已经讨论过,在服务器下会有特性(Characteristic),特性下会有值和其他内容。因此,当我们创建广告时,需要遵循这一原则。
// 健康温度计服务
const uint8_t thermometer_service_uuid[] = { 0x09, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(thermometer_service_uuid),
thermometer_service_uuid,
&service_handle);
app_assert_status(sc);
// 温度测量特性
const sl_bt_uuid_16_t temp_measurement_characteristic_uuid = { .data = { 0x1C, 0x2A } };
uint8_t temp_initial_value[5] = { 0, 0, 0, 0, 0 };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
service_handle,
SL_BT_GATTDB_CHARACTERISTIC_INDICATE,
0x00,
0x00,
temp_measurement_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
5,
5,
temp_initial_value,
&temp_measurement_characteristic_handle);
app_assert_status(sc);
在上述程序中,您可以看到 sl_bt_gattdb_add_service()
用于创建服务器。参数是一个特定的 UUID:0x1809。根据 GATT 的规则,0x1809 表示温度计类型数据,而同一特性的 UUID:0x2A1C 也有特殊含义。在 GATT 中,它表示温度测量。这与我们的温度值情况相符,因此这里我将其定义为这样。您可以在这里阅读 GATT 为我们准备的一些特定 UUID 的含义。
当然,您也可以设置不遵循 GATT 标准的 UUID,只需确保这两个值是唯一的,并且不会影响客户端通过识别这些 UUID 找到值。您可以访问 uuidgenerator.net 来为您的服务和特性创建随机 UUID。
最后,我们在 loop
中每秒测量一次 MCU 的温度值并进行广告。
接下来是客户端程序,这部分看起来会复杂得多。
在程序的开头,仍然是非常熟悉的内容。您需要确保这些内容与服务器端配置的一致。
const uint8_t thermometer_service_uuid[] = { 0x09, 0x18 };
const sl_bt_uuid_16_t temp_measurement_characteristic_uuid = { .data = { 0x1C, 0x2A } };
const uint8_t advertised_name[] = "XIAOMG24_BLE";
接下来,我们将编写一个蓝牙堆栈事件处理函数,主要处理由各种蓝牙事件触发的回调任务,包括蓝牙设备的初始化、蓝牙的连接和断开,以及搜索附近的蓝牙设备。
/**************************************************************************//**
* 蓝牙堆栈事件处理器
* 当蓝牙堆栈上发生事件时调用
*
* @param[in] evt 来自蓝牙堆栈的事件
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
以下过程是服务器中查找温度值的关键。首先,在我们成功定位服务器 UUID 并找到服务器下的特性 UUID 后,我们将处理获取的数据,如以下代码片段所示。最后,通过串口打印出处理后的数据。这种解析方法与蓝牙的数据结构是一一对应的。
void sl_bt_on_event(sl_bt_msg_t *evt)
{
sl_status_t sc;
switch (SL_BT_MSG_ID(evt->header)) {
...
// 当接收到特性值(例如指示)时,会触发此事件
case sl_bt_evt_gatt_characteristic_value_id:
{
Serial.println("接收到 GATT 数据");
// 从事件中获取接收到的数据
uint8_t* char_value = &(evt->data.evt_gatt_characteristic_value.value.data[0]);
// 将其转换回浮点数
float temperature = translate_IEEE_11073_temperature_to_float((IEEE_11073_float *)(char_value + 1));
// 打印到串口
Serial.print("接收到的温度:");
Serial.print(temperature);
Serial.println(" °C");
sc = sl_bt_gatt_send_characteristic_confirmation(evt->data.evt_gatt_characteristic_value.connection);
app_assert_status(sc);
}
break;
...
}
}
上述示例提供了一个单传感器单值的最简单示例,来源于 Silicon Labs。如果您希望更深入地了解 SiliconLabs BLE API 的使用,我们建议阅读以下教程。
技术支持与产品讨论
感谢您选择我们的产品!我们致力于为您提供各种支持,以确保您使用我们的产品时能够获得尽可能顺畅的体验。我们提供多种沟通渠道,以满足不同的偏好和需求。