项目由两台设备组成,其中一台是控制设备(本例中是手机),另一台是远程可穿戴设备。远程设备有 LED 指示它何时超出范围。控制设备打开连接,并定期发送测量RSSI的数据包。
项目所需的硬件是 Nordic Semiconductors 蓝牙开发套件之一。以下所有步骤均在 nRF5340-DK 上完成。
在开始之前,最好遵循设置软件环境的指南。
代码是在 nRF Connect SDK v1.5.0 上开发的,带有修补的 zephyr。该补丁是添加对 LCD 显示的支持所必需的,可在NordicPlayground github repo中找到。可能较新版本的 SDK 已经支持板dts 文件中的 Arduino 标头定义。
首先,我们将通过简单的步骤让代码运行,然后我们将深入了解 SDK 细节。
显示屏应显示 RSSI 标签和 RSSI 图形背景。
出于测试目的,LED 可以通过相当大的 (>= 10k ohm) 限流电阻器直接连接到端口输出。
在接下来的章节中,我们尝试添加尽可能多的信息,以帮助其他人了解一切在幕后是如何运作的。了解北欧设备的开发、使用 Zephyr OS 和使用蓝牙本身是一段相当长的旅程。
为了更容易理解,我们首先展示了工作代码的描述,然后有一些或多或少成功的步骤引导我们找到了这个特定的解决方案。整体学习部分需要几个星期的下班后实验才能开始。
配置项目、了解设备树文件、覆盖文件、了解蓝牙参数等方面的学习曲线非常陡峭。我们不打算在此处提供有关这些主题的完整教程。
SDK 附带的 Nordic 示例大多是开箱即用的。这是一个令人鼓舞的开始,但后来被 Arduino 宠坏了,我们认为复制粘贴代码足以将部分示例添加到我们的代码中。这是第一个让我们损失 2-3 周的错误。仅复制源代码是不够的,还有项目配置文件,并且通常需要在示例代码之上进行大量自定义。
最终代码是使用常规连接开发的,DK 作为“外围”设备,电话作为“中央”设备。回想起来,我们可能会使用 BluetoothLE 的 Broadcaster-Observer 角色,因为快速原型平台(应用程序发明者、flutter)对无连接数据传输的支持很差或不支持。
第一个也是最困难的部分是找到正确的配置设置。设置我们最终启用蓝牙外围设备:
# Incresed stack due to settings API usage
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_BT=y
CONFIG_BT_DEBUG_LOG=y
CONFIG_BT_SMP=y
CONFIG_BT_SIGNING=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DIS=y
CONFIG_BT_ATT_PREPARE_COUNT=5
CONFIG_BT_PRIVACY=y
CONFIG_BT_DEVICE_NAME="Otown"
CONFIG_BT_DEVICE_APPEARANCE=833
配置保存在prj.conf
. 这是迄今为止最神秘的部分,并且在刚开始使用 Zephyr 时文档记录很少。对我们有用的是从示例、文档和 Zephyr 源代码中复制的配置组合。
之后启用和启动蓝牙非常简单,并且在所有示例中看起来基本相同。所有的魔法都发生在从配置设置自动生成的代码中。
int err = bt_enable(NULL);
if (err) {
LOG_ERR("Bluetooth init failed (err %d)\n", err);
return;
}
任何面向连接的蓝牙链接的第一部分都是设置广告细节。为此,Zephyr 中有一些非常复杂的宏。这是一个对我们有用的结构:
//Unique Universal ID of service
#define OTOWN_UUID BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x39342d62, 0x3932, 0x662d, 0x6538, 0x313134343332))
// Advertising details for just one service, and generally discoverable peripheral
static const struct bt_data advertising_data[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_128_ENCODE(0x39342d62, 0x3932, 0x662d, 0x6538, 0x313134343332)),
};
// Bluetooth connect and disconnect callbacks
static struct bt_conn_cb conn_callbacks = {
.connected = connected,
.disconnected = disconnected,
};
...
// register connect and disonnect callbacks
bt_conn_cb_register(&conn_callbacks);
// Pass structure to bt_le_adv_start method
err = bt_le_adv_start(BT_LE_ADV_CONN_NAME, advertising_data, ARRAY_SIZE(advertising_data), NULL, 0);
我们使用从许多在线生成器之一生成的 UUID。对于自定义通信通道,它们基本上可以是连接双方都知道的随机值。
在最基本的层面上,蓝牙由服务组成,这些服务进一步分解为可以读取或写入的特性。
每个对象都有很多配置参数。在我们的项目中,我们使用了具有 2 个特征的单个服务。一种具有读/写方法,另一种是只写。为简单起见,访问特征没有加密或任何特殊配对要求。
#define REMOTE_RSSI_CHARACTERISTIC_UUID BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x63342d31, 0x3836, 0x372d, 0x3166, 0x306331633562))
#define DETACH_CHARACTERISTIC_UUID BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x1e086d95, 0x7faa, 0x4993, 0x984e, 0xcf234cec373b))
/* Primary Service Declaration */
BT_GATT_SERVICE_DEFINE(otown_svc, //create a struct with _name
BT_GATT_PRIMARY_SERVICE(OTOWN_UUID), //Main UUID
BT_GATT_CHARACTERISTIC(REMOTE_RSSI_CHARACTERISTIC_UUID,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, // Properties
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, // permissions read/write no security
read_otown, write_otown, otown_value), // Callback functions and value
BT_GATT_CHARACTERISTIC(DETACH_CHARACTERISTIC_UUID,
BT_GATT_CHRC_WRITE, // Properties
BT_GATT_PERM_WRITE, // permissions write no security
NULL, write_detach, detach_request), //Callback functions and value
BT_GATT_CCC(vnd_ccc_cfg_changed, //Client Configuration Configuration
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT),
);
处理这些特征的整个代码是自动生成的。
写入和读取特性通过回调函数发生。在这些回调中不要使用太多时间是非常重要的。在回调中更新 LCD 显示会在几秒钟后导致连接不稳定。日志输出似乎是可以容忍的。
写入时,数据片段必须存储在缓冲区中:
//Callback function of write command
static ssize_t write_otown(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset,
uint8_t flags) {
uint8_t *value = attr->user_data;
if (offset + len > sizeof(otown_value)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
memcpy(value + offset, buf, len);
然后可以解析接收到的字符串并将其传递给主应用程序。在这种情况下,使用了 Zephyr 消息队列。
int value_int = atoi(value);
k_msgq_put(&rssi_queue, &value_int, K_NO_WAIT);
return len;
}
消息队列是固定大小的循环缓冲区,提供应用程序线程之间的通信方式。这是一个简单的例子:
// Queue for passing received RSSI values to main thread (4 elements)
K_MSGQ_DEFINE(rssi_queue, sizeof(int), 4, 4);
...
// write callback on Bluetooth thread
k_msgq_put(&rssi_queue, &value_int, K_NO_WAIT);
...
// main thread - get value from queue, and display on LCD
int rssi;
if(k_msgq_get(&rssi_queue, &rssi, K_NO_WAIT) == 0) {
LOG_INF("RSSI = %d", rssi);
gui_add_point_to_chart(rssi);
}
只是改变状态的更简单的方法不需要使用队列。这是分离特征的写回调代码
#define DETACH_COMMAND "detach"
static ssize_t write_detach(...) {
...
// compare received string against predefined command
if(strncmp(value, DETACH_COMMAND, strlen(DETACH_COMMAND)) == 0) {
...
detached_safely = true;
}
return len;
}
最后断开回调负责检查电话是否“安全”断开连接
static void disconnected(...) {
...
// turn on red leds if remote device did not detach safely before disconnecting
if(!detached_safely) {
gpio_set_red(true);
}
}
必须在项目配置文件中启用第一个 GPIO 库
CONFIG_GPIO=y
使用 GPIO 通常需要在电路板覆盖文件中定义端口,但是有一个可用于原型设计的快捷方式
#define RED_LED_PIN 30
// "guess" that port 0 is named GPIO_0 on nRF boards
gpio = device_get_binding("GPIO_0");
if (gpio == NULL) {
printk("error getting GPIO_0 device\n");
return;
}
// configure pin 30 as an output
ret = gpio_pin_configure(gpio, RED_LED_PIN, GPIO_OUTPUT);
...
// set output
gpio_pin_set(gpio, RED_LED_PIN, true);
lvgl 库支持 Adafruit 2.8" LCD 显示器(在适当的板配置后)。尽管在我们的项目中不是绝对必要的,但它很有趣,并提供了很好的调试机会。
在编写这个项目时,有用于屏幕布局的 GUI 设计器的概念证明,但是代码生成器还没有准备好,可用的 GUI 组件很少。我们使用的代码大部分是从 NordicPlayground 上的 Nordic 示例中复制而来的。
图形组件的文档不是很好,经常需要查看源代码。除此之外,有时设置组件属性的顺序很重要。在正面的触摸屏上,显示与 Nordic 和 Zephyr 示例代码没有问题。
我们必须在项目文件中设置一些配置选项以启用 LVGL 支持
# LVGL DISPLAY
CONFIG_HEAP_MEM_POOL_SIZE=16384
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_DISPLAY=y
CONFIG_DISPLAY_LOG_LEVEL_ERR=y
CONFIG_LVGL=y
CONFIG_LVGL_ANTIALIAS=y
CONFIG_LVGL_USE_LABEL=y
CONFIG_LVGL_USE_CONT=y
CONFIG_LVGL_USE_BTN=y
CONFIG_LVGL_USE_CHECKBOX=y
CONFIG_LVGL_USE_IMG=y
CONFIG_LVGL_USE_THEME_MATERIAL=y
CONFIG_LVGL_USE_ANIMATION=y
CONFIG_LVGL_USE_SHADOW=y
CONFIG_LVGL_USE_CHART=y
CONFIG_LVGL_CHART_AXIS_TICK_LABEL_MAX_LEN=256
CONFIG_NEWLIB_LIBC=y
具体显示必须在CMakeLists.txt中选择
set(SHIELD adafruit_2_8_tft_touch_v2)
所有组件配置代码,包括 GUI 组件的一些实验都可以在gui.c
手机应用程序非常简单,功能仅限于扫描附近的蓝牙设备,然后在附加到设备后发送带有数据的字符串。
定期测量连接设备的 RSSI 并将其写入“RSSI”特性。这解决了 nRF SDK 上的问题,即一旦连接到中央设备,就无法在外围设备上读取 RSSI。
按下分离按钮将向“分离”特性发送“分离”命令。
对于手机应用程序,我们最初计划使用 Flutter,因为它具有原生的跨平台支持,但是,缺乏适当的 ble 库导致我们在更简单的东西上进行原型设计。起初,我们想使用 App Inventor 快速制作原型用于测试目的,虽然它一开始看起来很幼稚且不通用,但讽刺的是,它支持的 BLE 功能比任何可用的 Flutter BLE 库都多(例如从一个已经连接的设备),所以我们决定使用它。
另一个挫折是尝试从连接的设备获取 nRF SDK 中的 RSSI。RSSI 在扫描阶段很容易获得,但是在建立连接后无法获取。在对网络处理器代码和 HCI 接口进行修改时,我们陷入了死胡同。我们尝试使用 hci_pwr_ctrl 示例,其中蓝牙控制器(在 DK 情况下为网络核心)将 RSSI 值隧道传输到第二个核心上的应用程序线程。不幸的是,我们没有让这个示例工作,因为 nRF 的 Zephyr SDK 中显然存在一个已知错误。对于初学者来说也太高级了。
最初,我们想使用 nRF5340-DK 作为中央设备,以及简单的钥匙查找器蓝牙信标
我们找不到一个好的参数组合来保持与信标的连接。我们尝试了多个安全/配对参数,但在短暂的协商阶段后连接几乎立即断开。错误代码不是很有帮助,因此路径被删除了。
BluetoothLE 无连接广播者-观察者角色非常有前途。我们基于 Zephyr 示例在 nRF5340-DK 和 nRF52840 加密狗之间进行了简单的广告设置,但是我们无法轻松传输任何有意义的数据。所有修改都导致代码失败。可能拥有 2 个完整的开发套件会更容易。手机应用程序原型设计框架中缺乏对这些角色的支持也导致了这条路的放弃。
一旦我们找到一些时间对其进行重新测试和清理,其中一些实验的代码将在 GitHub 存储库中提供。
在学习 Zephyr 时,我们为 MAX6675 热电偶 ADC 开发了一个简单的 SPI 驱动程序。它作为 Zephyr 2.4.99 的补丁提供(随 nRF SDK 1.5.0 提供)。
从 Play 商店获取nRF Connect应用程序非常有帮助。它非常适合获取有关外围设备的详细信息。非常稳定且功能丰富的蓝牙连接调试。然而,我们无法确定是否可以将其用作 Brodcaster 或 Observer。
在带有外部 TTL 到 USB 转换器的 nRF52840 加密狗上获得调试接口取得了一些成功。
默认情况下调试输出被禁用。要将其重定向到串行端口(默认情况下,引脚 0.20 上的 TX,引脚 0.24 上的 RX)在项目配置中启用 SERIAL 和 UART_CONSOLE。
配置用于调试的 USB 接口最初看起来很简单,但最终它只适用于 Zephyr USB 日志记录示例。当配置和代码被复制粘贴到我们的应用程序时,它在第一个日志记录宏上失败了。
这是一些我不记得它来自哪里的随机注释,但是在从 Zephyr 为 nRF52840 加密狗构建蓝牙示例时它非常重要:
启用 FLASH 设置。要控制蓝牙设备名称,请启用设置和 NVS。然后可以更改设备名称。
在 AppInventor 方面:确保您没有使用 2019 年以来过时的 BLE 插件,较新的 android 设备无法在其上运行,因为操作系统受到更多限制,但它已通过 2020 年 12 月插件修复。
有时应用程序会弄乱手机上的蓝牙子系统,显示一堆错误。关闭应用程序和禁用->启用周期有助于让事情重回正轨。
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
全部0条评论
快来发表一下你的评论吧 !