3 教程
下面的教程将一步一步地讲解 SDK 里的基本概念,核心工具的使用方法。
3.1 世界你好
在本教程里,我们将创建一个设备,发送的广播里带着它的名字:“Hello, 世界”。
从开始菜单里打开 ingWizard
,选择菜单 Project
-> New Project ...
。
这个菜单项会打开项目向导。向导的第一页是 Development Tool
(见图
3.1)。
3.1.1 Development Tool
页面
在这个页面 (图 3.1):
- 选择 IDE/工具链
- 设置项目名称
- 选择项目的存储位置
ingWizard
提供以下便利功能:
如果需要用
Git
做软件版本管理,选择Setup .gitignore
;如果准备使用
Visual Studio Code
作为代码编辑器,选择Setup Visual Studio Code
。
然后点击 Next
按钮进入下一页,Choose Chip Series
。
3.1.5 Peripheral Setup
页面
在这个页面(图 3.5),选择 Legacy
广播方式。
支持 BLE 5.x 扩展广播的手机目前依然凤毛麟角,即使声称了“支持” BLE
5.0。为了更好的兼容性,这里我们选择 使用 Legacy
广播。此外,待项目创建之后,通过修改一个比特就能将 Legacy
广播切换成 BLE 5.x 扩展广播。
点击 Setup Advertising Data
按钮, 打开广播数据编辑器(图
3.6)。 在编辑器里,输入 name
可以快速定位到 GAP
广播项 09 - «Complete Local Name»
,点击 Add
将这个项添加到设备的广播数据里。
点击刚刚添加的 09 - «Complete Local Name»
数据项,
然后将在下面的数据编辑框里输入 “Hello, 世界” 并按回车。 Data Preview
会更新,整个广播都将已原始字节流的形式显示其中。显而易见,工具对中文字符使用了
UTF-8
编码。
现在点击 OK
回到项目向导,然后点击 Next
进入下一页,Security & Privacy
。
3.1.9 编译您的项目
回到 ingWizard
的主窗口(图
3.10),点击打开您的项目。在 IDE
里编译您的项目。
3.2 iBeacon
在本教程中,让我们创建一个iBeacon。iBeacon是由Apple11
开发的协议,并在2013年的苹果全球开发者大会上发布。Beacons是一类低功耗蓝牙(BLE)设备,可以将其标识符广播到附近的便携式电子设备。这项技术使智能手机、平板电脑和其他设备在接近iBeacon设备时能够执行相应的操作。
首先,从app
Store下载一个iBeacon扫描应用程序。在本教程中,我们将使用一个名为
Locate
的应用程序。 Locate
有一个预配置的近距离UUID列表,其中包括一个全0 Null
UUID。我们将使用这个Null UUID12。
3.2.1 建立广播数据
iBeacon广播包中有两个项目。
Flags
值固定为0x06,即设置了两位,LE General Discoverable Mode & BR/EDR Not Supported。
Manufacturer Specific Data
本项目内容如表 3.1所示
Size in Bytes | Name | Value | Notes |
---|---|---|---|
2 | Company ID | 0x004C | Company ID of Apple, Inc |
2 | Beacon Type | 0x1502 | Value defined by Apple |
16 | Proximity UUID | User defined value | |
2 | Major | Group ID | |
2 | Minor | ID within a group | |
1 | Measured Power | in dBm | Measured by an iPhone 5s at a 1 meter distance |
为了制作一个iBeacon设备,我们可以遵循[Hello World]示例中的相同步骤,我们只有在个别情况下需要根据规范配置广播包。
在广播数据编辑器中,添加 0x01 - «Flags»
和
0xFF - «Manufacturer Specific Data»
.单击 0x01 - «Flags»
,检查
LE General Discoverable Mode
和 BR/EDR Not Supported
。单击0xFF - «Manufacturer Specific Data»
,然后点击 Edit as
按钮,会有一个菜单弹出并选择 iBeacon ...
(图3.13) 来打开iBeacon厂商特定数据编辑器 (图
3.14).
信号功率可以设置为任何合理值(如-50dBm),稍后我们将在 Locate
应用程序的帮助下对其进行校准。
3.2.2 尝试应用
让我们在 Choose Project Type
页面上选择GNU Arm Embedded
Toolchain作为我们的开发环境,向导会让一切准备就绪 (图 3.15).
单击项目以打开控制台,输入 make
13来构建它。回到 ingWizard
,按照相同的步骤将其下载。现在,我们可以在 Locate
中找到新创建的iBeacon设备。 (图 3.16)
点击我们的设备,我们就可以实时校准信号功率或检查距离,如图 3.17所示。
一旦信号功率校准,我们可以在 ingWizard
中右键单击我们的项目,并选择
Edit Data
-> Advertising
菜单项,以用我们所熟悉的编辑器来编辑其广播数据。更新广播数据后,重新构建项目,检查距离是否更准确。
根据规范,接近beacons必须使用一 个不可连接的无定向广播PDU,使用固定的100ms广播间隔。在本教程中,我们不会去修改代码,所以广播参数也不会被修改。为了使这些参数完全符合规范,请参考相应的主机GAP APIs。
3.3 温度计
在本教程中,我们将制作一个 重要
的BLE设备,一个温度计。蓝牙SIG已经定义了一个称为健康温度计的GATT服务14。这个SDK包含一个名为
INGdemo
的参考APP,它可以安装到 Android
或 iOS
设备上。使用
INGdemo
,我们可以查看蓝牙设备的广播数据,如果设备中有健康体温计功能,
INGdemo
可以连接设备并读取温度。
在本教程中,您将了解如何:
- 广播所支持的服务
- 配置GATT配置文件
- 响应GATT特性的读请求
3.3.1 建立广播数据
同样,我们遵循与[Hello World]示例中相同的步骤,并在 Peripheral Setup
页面上声明温度计服务并创建GATT配置文件。在广播数据中添加以下三项:
Flags
值固定为0x06,即设置了两位,LE General Discoverable Mode & BR/EDR Not Supported。
Complete List of 16-bit Service Class UUIDs
添加一个如图3.18.所示的
0x1809 - Health Thermometer
服务。Complete Local Name
让我们将设备命名为”AccurateOne”。
3.3.2 建立GATT配置文件
返回 Peripheral Setup
页面,单击 Setup ATT database ...
打开GATT配置编辑器。添加两个服务,General Access (0x1800) 和Health
Thermometer (0x1809)。 删除General Access
service的所有非必选特性。对于Health Thermometer
service保留两个特性,即温度测量和温度类型,删除其他两个。
接下来,编辑每个特性的值:
Device Name of General Access:
右键单击特征,选择
Edit String Value ...
菜单,并设置值为”AccurateOne”。Appearance of General Access:
右键单击特性,选择
Help
,编辑器将在Bluetooth SIG网站上打开相应的文档。找到普通温度计的值(0x0300),然后单击Edit
按钮,并在数据字段中输入0x00, 0x03
。Temperature Measurement of Health Thermometer
请查看蓝牙SIG网站文档。点击
Edit
按钮并在data字段中输入5个0(0, 0, 0, 0, 0
)。这里的第一个字节包含标志,表明之后的度量单位是一个以摄氏度为单位的FLOAT
值。检查read
和dynamic
属性(图 3.19)。FLOAT
类型为IEEE-11073 32位浮点。最基本的是,它有一个24位的尾数和一个8位的指数(最重要的字节),以 10 为基数。Temperature Type of Health Thermometer
请查看Bluetooth SIG网站文档。通过单击
Edit
按钮将其设置为任何有效值。
3.3.3 代码编写
项目创建完成后,在IDE中打开 profile.c
,由 ingWizard
自动生成温度测量特征处理函数 att_read_callback
。
static uint16_t att_read_callback(hci_con_handle_t connection_handle,
uint16_t att_handle, uint16_t offset,
uint8_t * buffer, uint16_t buffer_size)
{
switch (att_handle)
{
case HANDLE_TEMPERATURE_MEASUREMENT:
if (buffer)
{
// add your code
return buffer_size;
}
else
return 1; // TODO: return required buffer size
default:
return 0;
}
}
当APP读取一个具有 dynamic
特点的特性, att_read_callback
会被调用两次或更多:一次用于查询所需的缓冲区大小,
一次用于读取数据。如果数据很大, att_read_callback
可能会被调用更多次,每次读取由 offset
指定的部分数据。
如上所述,定义一种温度测量类型:
typedef __packed struct gatt_temperature_meas
{
;
uint8 flags:24;
sint32 mantissa:8;
sint32 exponent} gatt_temperature_meas_t;
static gatt_temperature_meas_t temperature_meas = {0};
现在,我们可以完善上面的 case HANDLE_TEMPERATURE_MEASUREMENT
语句:
case HANDLE_TEMPERATURE_MEASUREMENT:
if (buffer)
{
// simulate an "accurate" thermometer
.mantissa = rand() % 100;
temperature_meas// output data
(buffer, ((uint8 *)&temperature_meas) + offset, buffer_size);
memcpyreturn buffer_size;
}
else
return sizeof(gatt_temperature_meas_t);
构建并下载项目,然后在 INGdemo
app中连接到”AccurateOne”设备。检查每次按下 Refresh
按钮时温度是否随机变化 (图 3.20).
温度计(服务器)可以使用通知或指示过程来通知(不需要确认)或指示(需要确认)一个特征值,参见[带通知的温度计]。在本例中,“AccurateOne”不使用这两个过程,而是被动地发送其测量结果。
3.4 FOTA温度计
在本教程中,我们将在我们的温度计中添加无线固件升级(FOTA)功能。此SDK提供了一个可开箱即用的FOTA设计参考。要使FOTA工作,至少需要涉及三个部分,一个设备、一个APP和一个HTTP服务器。
INGdemo
应用已经有了,所以在本教程中,我们将重点关注设备和HTTP服务器。
3.4.1 FOTA设备
按照与前面温度计示例相同的步骤创建一个新项目,命名为”ota”。
当编辑广播数据时,我们可以通过单击在编辑器中的 Open File…
按钮,来导入在前面例子中创建的数据。广播数据存储在
$(ProjectPath)/data/advertising.adv
.让我们将设备命名为”clickty
Click”。
当编辑GATT协议数据库时,我们可以通过单击在编辑器中的打开文件
Open File…
按钮来导入在前面例子中创建的数据。GATT协议的数据存储在
$(ProjectPath)/data/gatt.profile
.中。在 Add Service
按钮的下拉菜单中选择 INGChips Service
,添加”INGChips FOTA
Service”,接下来,编辑该服务的特征值:
FOTA Version:
FOTA Version确定了我们项目的完整版本号。如flash downloader所示,整个项目由两个二进制文件组成,一个来自SDK 软件包,称为平台二进制文件;另一个来自我们的项目,称为APP二进制文件。FOTA版本包含两个子版本,它们分别对应一个二进制文件。每个子版本包含三个字段:
Major: A 16-bit field.
Minor: A 8-bit field.
Patch: Another 8-bit field.
每个软件包都有自己的版本(同平台二进制文件版本),使用相同的编号方案,可以在
SDK
页面的Environment Options
对话框中找到(使用菜单项Tools
->Environment Options
打开此对话框)。假设平台版本为1.0.1,我们想要的APP版本为1.0.0,那么我们将该特征值设为(图 3.21):0x0001, 0, 1 // platform version 0x0001, 0, 0, // app version
FOTA Control
这是升级期间的控制点。将其值设置为
0
(即OTA_STATUS_DISABLED
),这是FOTA的初始状态。
单击 OK
关闭 GATT
协议编辑器。(注意:不要点击 Save
,除非你想改变已经在编辑器中打开的 $(ProjectPath)/data/gatt.profile
。)
回到项目向导,按下 Next
继续下一页的 Firmare Over-The-Air
。在这页上,我们勾选 FOTA
。注意,与FOTA相关的特征句柄是通过检查GATT协议自动生成的。然后在项目向导上完成其余步骤。
打开我们新的项目”ota”,从上一个例子复制代码来使我们的温度计在 INGdemo
APP中可以响应 Refresh
。
接下来,让我们制作一个新版本。
3.4.2 创建一个新版本
我们”ota”的新版本命名为”Barba Trick”,APP版本号将升级到 2.0.0
。这些数据分别保存在广播数据和协议数据中,右键单击项目并使用编辑器更新它。数据升级以后,使用
Save As ...
将数据保存在相同路径下的另一个文件中。例如,更新广播数据并将其保存到
$(ProjectPath)/data/advertising_2.adv
。并更新GATT配置文件到
$(ProjectPath)/data/gatt_2.profile
。
用一个宏 V2
来控制实际用到的广播数据和协议数据:
const static uint8_t adv_data[] = {
#ifndef V2
#include "../data/advertising.adv"
#else
#include "../data/advertising_2.adv"
#endif
};
......
const static uint8_t profile_data[] = {
#ifndef V2
#include "../data/gatt.profile"
#else
#include "../data/gatt_2.profile"
#endif
};
用定义的 V2
宏重新编译项目,将 ota.bin
和 platform.bin
(在
SDK_DIR/sdk/bundles/typical
中)复制到一个空路径下,如ota_app_v2
。
在 ota_app_v2
创建一个名为 manifest.json
的文件,包含以下数据:
{"platform": {
"version": [1,0,1],
"name": "platform.bin",
"address": 16384
,
}"app": {
"version": [2,0,0],
"name": "ota.bin",
"address": 163840
,
}"entry": 16384,
"bins":[]
}
这些地址可以在 Environment Options
. entry
中找到。地址值固定为
0x4000
,即 16384
。注意 json
不识别常规的 0xabcd
十六进制文字。
INGdemo
可以下载其他 bin
指定的二进制文件到设备。在本例中,我们没有这样的二进制文件,所以这个字段作为空数组保留。
然后为这个更新创建一个 readme
文件,其中包含一些关于本次更新的信息。
现在FOTA包已经准备好了。为整个 ota_app_v2
路径制作一个
ota_app_v2.zip
的ZIP压缩包。注意,不应该将 ota_app_v2
设置为
ota_app_v2.zip
中的子目录。表 3.2
给出了压缩文件中的文件清单。
File Name | Notes |
---|---|
readme | Some information about this update |
manifest.json | Meta information |
platform.bin | Platform binary |
ota.bin | App binary |
回到IDE,在没有定义宏 V2
的情况下重新构建项目,然后下载该项目到开发板。
3.4.3 FOTA服务器
INGdemo
APP需要一个FOTA服务器URL,定义在
class Thermometer.FOTA_SERVERr
。将 ota_app_v2.zip
移动到HTTP服务器的文档目录,并创建一个 latest.json
文件,它包含最新版本的信息。内容是:
{"app": [2,0,0],
"platform": [1,0,1],
"package": "ota_app_v2.zip"
}
确保这两个文件可以通过 (FOTA_SERVER + latest.json)
和
(FOTA_SERVER + ota_app_v2.zip)
访问.
3.4.4 尝试应用
在 INGdemo
中连接”Clickety Click”,点击 Update
(图
3.22). 由于 platform.bin
是最新的,所以只需要升级 app.bin
就可以了,整个升级过程在很短的时间内完成。回到主页,再次扫描并检查我们的新版本是否起作用,有了一个名为”Barba
Trick”的设备。连接到”Barba Trick”,可以看到现在固件是最新的。
本教程给出了一个实现FOTA的例子。用户可以自由设计新的FOTA解决方案,从版本定义到FOTA服务和特性。也可以为FOTA开发一个专用的第二APP。
安全性 是必须要考虑的。
3.5 iBeacon扫描设备
我们已经知道如何配置iBeacon设备。在本教程中,我们将创建一个iBeacon扫描器。
扫描器在蓝牙微距网络中起着核心作用。和之前一样,我们在 ingWizard
中创建一个名为”iscanner”的新项目 (图 3.23). 在
Role of Your Device
页面,选择Central。中心设备会一直扫描其他设备然后这些设备执行其相应的动作,我们的新项目向导自动添加代码开始扫描。
在IDE中打开这个新项目,并找到函数 user_packet_handler
。我们可以看到有一个名为 HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT
的事件:
case HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT:
{
const le_ext_adv_report_t *report = decode_hci_le_meta_event(packet,
)->reports;
le_meta_event_ext_adv_report_t// ...
}
break;
每次接收到这个事件时,我们可以检查广播报告是否包含
0xFF - «Manufacturer Specific Data»
,以及它是否是一个iBeacon包。有了制作iBeacon设备的知识,就可以直接用
C
语言定义一个iBeacon数据包的类型。
typedef __packed struct ibeacon_adv
{
uint16_t apple_id;
uint16_t id;
uint8_t uuid[16];
uint16_t major;
uint16_t minor;
int8_t ref_power;
} ibeacon_adv_t;
#define APPLE_COMPANY_ID 0x004C
#define IBEACON_ID 0x1502
__packed
是一个扩展关键字,用于指定数据类型使用第一种数据对齐方式。幸运的是,
ARM 和 IAR 编译器都支持它。或者可以使用 #pragma pack
指令:
#pragma pack (push, 1)
typedef struct ibeacon_adv
{
...
} ibeacon_adv_t;
#pragma pack (pop)
在继续之前,让我们创建一个将UUID转换为字符串的辅助函数。
const char *format_uuid(char *buffer, uint8_t *uuid)
{
(buffer, "{%02X%02X%02X%02X-%02X%02X-%02X%02X-"
sprintf"%02X%02X-%02X%02X%02X%02X%02X%02X}",
[0], uuid[1], uuid[2], uuid[3],
uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9],
uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]);
uuidreturn buffer;
}
3.5.1 估算距离
接收到的信号强度指示(RSSI)与广播数据一起播报。一般来说,点源辐射出的电磁波强度与信号源距离的平方成反比。著名的自由空间损失方程为:
\[ Loss = 32.45 + 20log(d) + 20log(f) \]
其中 \(d\) 的单位是km, \(f\) 的单位是MHz, \(Loss\)
的单位是dB。通过比较RSSI和1米距离下的测量功率(ref_power
),我们可以利用自由空间损失方程大致估计出扫描仪和信标之间的距离:
double estimate_distance(int8_t ref_power, int8_t rssi)
{
return pow(10, (ref_power - rssi) / 20.0);
}
现在,我们用不到20行代码就可以实现一个功能完整的iBeancon扫描器:
uint8_t length;
*p_ibeacon;
ibeacon_adv_t char str_buffer[80];
const le_ext_adv_report_t *report;
......
case HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT:
= decode_hci_le_meta_event(packet,
report )->reports;
le_meta_event_ext_adv_report_t= (ibeacon_adv_t *)ad_data_from_type(report->data_len,
p_ibeacon (uint8_t *)report->data, 0xff, &length);
if ((length != sizeof(ibeacon_adv_t))
|| (p_ibeacon->apple_id != APPLE_COMPANY_ID)
|| (p_ibeacon->id != IBEACON_ID))
break;
("%s %04X,%04X, %.1fm\n",
printf(str_buffer, p_ibeacon->uuid),
format_uuid->major, p_ibeacon->minor,
p_ibeacon(p_ibeacon->ref_power, report->rssi));
estimate_distancebreak;
使用 Locate
应用程序发送iBeacon信号,并查看我们的设备是否能找到它
(图 3.24).
最后,由于RSSI值是波动的,可以在RSSI上增加一个低通滤波器使估算值更加稳定。
需要注意该APP的二进制文件的大小会急剧增加。这主要是因为Cortex-M3没有硬件浮点单元,浮点操作都是由库函数执行的。使用浮点运算前请 仔细考虑一下 。
3.6 通知&指示
服务器可以使用通知或指示过程来通知(不需要确认)或指示(需要确认)一个特征值。现在,让我们将通知和指示功能添加到我们在之前教程中创建的温度计中。
我们分别使用 att_server_notify
和 att_server_indicate
来通知或指示一个特征值。这些 API
必须在蓝牙协议栈 (Host
) 任务中调用。
主动产生的通知和指示可以由定时器或中断触发,例如蓝牙任务栈之外的源。SDK 提供了基于 RTOS 消息的任务间通信机制以便调用这些蓝牙协议栈 API。
3.6.1 任务间通信
可使用 btstack_push_user_msg
发送消息到蓝牙协议栈:
uint32_t btstack_push_user_msg(uint32_t msg_id, void *data, const uint16_t len);
这个消息将被传递到你的 user_packet_handler
下的事件ID
BTSTACK_EVENT_USER_MSG
:
static void user_packet_handler(uint8_t packet_type, uint16_t channel,
uint8_t *packet, uint16_t size)
{
uint8_t event = hci_event_packet_get_type(packet);
*p_user_msg;
btstack_user_msg_t if (packet_type != HCI_EVENT_PACKET) return;
switch (event)
{
// ......
case BTSTACK_EVENT_USER_MSG:
= hci_event_packet_get_user_msg(packet);
p_user_msg (p_user_msg->msg_id, p_user_msg->data,
user_msg_handler->len);
p_user_msgbreak;
// ......
}
}
在这里,我们将用户消息的处理传递给另一个叫 user_msg_handler
的函数。注意 user_msg_handler
是在蓝牙协议栈任务的上下文中运行的,现在我们可以调用那些蓝牙栈APIs了。
事件 BTSTACK_EVENT_USER_MSG
被广播到所有 HCI
事件回调函数。
3.6.2 定时器
现在让我们让温度计”AccurateOne”每秒钟更新一次它的值。首先,在初始化时创建一个定时器,例如在
app_main
或 setup_profile
中。
= 0;
TimerHandle_t app_timer
uint32_t setup_profile(void *data, void *user_data)
{
= xTimerCreate("app",
app_timer (1000),
pdMS_TO_TICKS,
pdTRUE,
NULL);
app_timer_callback// ...
}
定时器回调函数可以被定义为:
#define USER_MSG_ID_REQUEST_SEND 1
static void app_timer_callback(TimerHandle_t xTimer)
{
if (temperture_notify_enable | temperture_indicate_enable)
(USER_MSG_ID_REQUEST_SEND, NULL, 0);
btstack_push_user_msg}
当我们在 HCI_EVENT_LE_META
中得到
HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE
时,定时器开始计时,并在我们得到 HCI_EVENT_DISCONNECTION_COMPLETE
时定时器停止。
这里的 temperture_notify_enable
和 temperture_indicate_enable
是两个初始化为 0s
的标志,并在 att_write_callback
中设置为 1
:
static int att_write_callback(hci_con_handle_t connection_handle,
uint16_t att_handle, uint16_t transaction_mode,
uint16_t offset, uint8_t *buffer, uint16_t buffer_size)
{
switch (att_handle)
{
case HANDLE_TEMPERATURE_MEASUREMENT + 1:
= connection_handle;
handle_send switch (*(uint16_t *)buffer)
{
case GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_INDICATION:
= 1;
temperture_indicate_enable break;
case GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION:
= 1;
temperture_notify_enable break;
}
return 0;
// ...
}
}
在这里,我们将 connection_handle
存储到一个全局变量 handle_send
,并在之后会使用该变量。最后一段代码是在 user_msg_handler
中处理消息
USER_MSG_ID_REQUEST_SEND
:
static void user_msg_handler(uint32_t msg_id, void *data, uint16_t size)
{
switch (msg_id)
{
case USER_MSG_ID_REQUEST_SEND:
(handle_send);
att_server_request_can_send_now_eventbreak;
}
}
并在 ATT_EVENT_CAN_SEND_NOW
中报告温度值:
...
case ATT_EVENT_CAN_SEND_NOW:
.mantissa = rand() % 100;
temperature_measif (temperture_notify_enable)
{
(handle_send,
att_server_notify,
HANDLE_TEMPERATURE_MEASUREMENT(uint8_t*)&temperature_meas,
sizeof(temperature_meas));
}
if (temperture_indicate_enable)
{
(handle_send,
att_server_indicate,
HANDLE_TEMPERATURE_MEASUREMENT(uint8_t*)&temperature_meas,
sizeof(temperature_meas));
}
break;
...
尝试重新构建并下载项目,并检查 INGdemo
中显示的温度值是否每秒变化一次。
有一个完整功能的温度计示例,即thermoota
,它支持FOTA,通知和指示。
3.7 吞吐量
BLE 5.0介绍了一种新的采样率为2M的非编码PHY。
3.7.1 理论峰值吞吐量
数据物理通道PDU的最大有效载荷长度为251字节。采用2M PHY,传输时间为1048 μs。一个空的数据物理通道PDU的传输时间为44 μs。
为了实现一个方向上的最大吞吐量,该方向上所有pdu的长度应该为251字节,而另一个方向上所有pdu的长度应该为空。所以,251个字节总的传输时间为:
\[ 1048 + 44 + 150 * 2 = 1392 (\mu s) \]
因此,链路层提供的理论峰值吞吐量为:
\[ 251 * 8 / 1392 * 1000000 \approx 1442.528 (kbps) \]
对于一个运行在GATT之上的应用程序,I2CAP和ATT都有它们自己的开销。通常,GATT的最大有效负载为(251 - 7 =)244字节。因此,GATT可以提供的理论上的峰值吞吐量为:
\[ 244 * 8 / 1392 * 1000000 \approx 1402.298 (kbps) \]
3.7.2 测试吞吐量
在SDK中有一对用于吞吐量测试的示例 (图 3.25).
3.7.2.1 对 INGdemo
进行测试
下载 peripheral_throughput
. 使用 INGdemo
连接到 ING Tpt
,
打开吞吐量测试页面。在这个页面上,我们可以测试从主到从、从到主或同时在两个方向上的吞吐量。
图中 (图 3.26)显示,使用支持2M PHY的普通Android手机,我们可以实现1M+ bps的空中传输吞吐量。
3.7.2.2 对我们的APP进行测试
示例 central_throughput
演示了BLE中心设备的一般工作过程:
- 扫描并连接到在其广播中声明了吞吐量服务的设备;
- 发现吞吐量服务;
- 发现服务的特性;
- 发现特性描述。
INGChips Throughput Service
有两个特点。
通用输出
通过这一特性,外围设备向中心设备发送数据。
这个特性有一个
Client Characteristic Configuration
描述符。通用输入
通过这个特性,中心设备向外围设备发送数据。
下载 central_throughput
到另一块板。这个应用程序有一个UART命令行接口给到主机。连接到主机,输入”?“以查看所支持的命令。这个APP自动连接到
peripheral_throughput
。输入命令 start s->m
或 start m->s
开始测试从外设到中心设备的吞吐量,或从中心设备到外设的。
图中(图 3.28)显示,使用两块板,我们在空中实现了1.2M+ bps的稳定数据传输。
这个吞吐量是在空中测试的,比理论峰值略低,但真实性更高。
3.8 双角色 & BLE网关
在本教程中,我们将创建一个BLE网关,它从几个外设收集数据并将数据报告给一个中心设备。在收集数据时,这个网关是一个中心设备,而报告数据时,它是一个外设,也就是说,我们的APP能够扮演双角色。
具体而言,我们的网关只支持从温度计收集数据。我们称之为 smart_meter
。
smart_meter
使用一个通用型基于字符串的输出服务将数据报告到中心设备,比如运行在智能手机上的
INGdemo
。它还拥有可以连接到主机的UART控制接口。
检查示例 peripheral_console
以了解如何进行字符串输入和输出。
我们同样提供了完整功能的 smart_meter
app作为示例。在创建您自己的示例时,请参考此示例。
现在,让我们创建这个BLE网关。
3.8.1 用 ingWizard
创建一个外设APP
使用GUI编辑器编辑广播数据,命名我们的应用程序为”ING Smart Meter”。
使用GUI编辑器编辑GATT配置文件。将 INGChips Console Service
添加到GATT配置文件 (图 3.30).
3.8.2 定义温度计数据
温度计由它的设备地址和id来识别。每个温度计使用由 conn_handle
来显示自己的连接状态。
typedef struct slave_info
{
uint8_t id;
;
bd_addr_t addruint16_t conn_handle;
;
gatt_client_service_t service_thermo;
gatt_client_characteristic_t temp_char;
gatt_client_characteristic_descriptor_t temp_desc;
gatt_client_notification_t temp_notify} slave_info_t;
设定了4个温度计。
3.8.3 扫描温度计
调用两个GAP
API接口开始扫描。一旦找到一个设备,会检查它的设备地址是否是温度计。如果是,停止扫描并调用
gap_ext_create_connection
进行连接。
连接建立后,如果有温度计未连接,则重新开始扫描。
3.8.5 数据处理
预定温度计的 Temperature Measurement
特性。当接收到一个新的测量值时,将该值转换为一个字符串并将其报告给主机。如果我们的应用程序已经连接到一个中心设备,通过GATT特性将该信息转发给它。
3.8.7 准备温度计
我们可以将示例 thermo_ota
用做温度计。但是我们需要为每一个温度计配置不同的地址。
我们可以写一个简单的脚本为下载程序自动生成这些地址:
procedure OnStartBin(const BatchCounter, BinIndex: Integer;
var Data: TBytes; var Abort: Boolean);
begin
if BinIndex <> 6 then Exit;
0] := BatchCounter;
Data[end;
有关下载脚本的更多信息,请参见 Scripting & Mass Production.
3.9 Hello, Nim
要使用 Nim
开发应用程序,需要 Nim
和 Gnu 工具链
。 Nim
编译器将
Nim
源代码翻译成 C
源代码,然后调用 Gnu工具链
将翻译后的 C
源代码与SDK进行编译链接,如图所示。(图 3.31).
推荐使用 Visual Studio Code
编辑和构建 Nim
代码。让我们使用 Nim
制作一个简单的应用程序。
3.9.1 创建一个 Nim
APP
在 Develpment Tool
页面,选择 Nim + Gnu Toolchain
。选择 By Code
生成广播和ATT数据库 (图 3.32).
ingWizard
也支持为 Nim
应用程序创建这些数据。在本教程中,我们将展示轻松地在 Nim
中使用元程序创建这些数据。
3.9.2 创建广播数据
使用 Nim
模块 btdatabuilder
,我们可以轻松创建广播和GATT配置文件。
例1:创建一个名为”Hello, Nim”的设备
let = ToArray([Flags({LEGeneralDiscoverableMode, BR_EDR_NotSupported}), advData LocalName("Hello, Nim")])
例2:创建iBeacon
let = ToArray([Flags({LEGeneralDiscoverableMode, BR_EDR_NotSupported}), advData iBeacon("{E9052F1E-9D67-4A6E-B2D7-459D132D6A94}", 0, 0, -50)])
3.9.3 创建配置数据
defineProfile([Service(SIG_UUID_SERVICE_GENERIC_ACCESS),
Characteristic(SIG_UUID_CHARACT_GAP_DEVICE_NAME, ATT_PROPERTY_READ,
"Hello, Nim"),
Characteristic(SIG_UUID_CHARACT_GAP_APPEARANCE, ATT_PROPERTY_READ,
[0u8, 0]),
Service(SIG_UUID_SERVICE_BATTERY_SERVICE),
Characteristic(SIG_UUID_CHARACT_BATTERY_LEVEL, ATT_PROPERTY_READ,
[20u8], "HANDLE_BATTERY_LEVEL")],
"profileData")
上述代码编译完成后,ATT数据库存储在 profileData
中,电池电量特性由常量
HANDLE_BATTERY_LEVEL
标识,电池电量值在ATT基础(即 profileData
)中的偏移量(以字节为单位)由常量 HANDLE_BATTERY_LEVEL_OFFSET
标识。
我们可以通过宏 defineProfile
像使用普通变量一样使用这些生成的变量和常量。例如,让我们创建一个任务来随机更新电池电量:
proc updateBatteryLevel(unused: pointer) {.noconv.} =
while true:
vTaskDelay(pdMS_TO_TICKS(1000))
[HANDLE_BATTERY_LEVEL_OFFSET] = rand_level()
profileData...
discard xTaskCreate(updateBatteryLevel, "b",
, nil,
configMINIMAL_STACK_SIZE- 1, nil) configMAX_PRIORITIES
在 Nim
中至少有三种方法可以生成伪随机数,使用 C
语言stdlib库提供的PRNG,使用 Nim
提供的PRNG,或者创建我们自己的PRNG。
- 使用
C
的PRNG
# It's easy to import C functions and use them
proc rand(): cint {. importc: "rand", header: "stdlib.h".}
proc rand_level(): uint8 = cast[uint8](rand() mod 101)
- 使用
Nim
的PRNG
import random
proc rand_level(): uint8 = cast[uint8](rand(0..100))
- 创建一个简易的PRNG
proc rand_level(): uint8=
var last {.global.} = 0u16
= (last * 173 + 31) and 0x7fffu16
last return cast[uint8](last mod 101)
正如我们所看到的,这三种方法在 Nim
上使用都很简单。
platform_hrng
可用于初始化PRNG。
Note that UUID is not allowed to be all 0s in final products.↩︎
Makefile
follows the syntax of GNUmake
.↩︎https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.health_thermometer.xml↩︎