1. 简介
Nest 发布的 OpenThread 是 Thread® 网络协议的开源实现。Nest 发布了 OpenThread,以便将 Nest 产品中使用的技术广泛提供给开发者,从而加快智能互联家居产品的开发速度。
Thread 规范定义了一种基于 IPv6 的可靠、安全且低功耗的无线设备到设备通信协议,适用于住宅应用。OpenThread 实现了所有 Thread 网络层,包括 IPv6、6LoWPAN、IEEE 802.15.4(具有 MAC 安全性)、网状链路建立和网状路由。
在此 Codelab 中,您将使用 OpenThread API 启动 Thread 网络、监控设备角色的变化并对其做出响应,以及发送 UDP 消息,并将这些操作与真实硬件上的按钮和 LED 相关联。
学习内容
- 如何对 Nordic nRF52840 开发板上的按钮和 LED 进行编程
- 如何使用常见的 OpenThread API 和
otInstance
类 - 如何监控 OpenThread 状态变化并对其做出响应
- 如何向 Thread 网络中的所有设备发送 UDP 消息
- 如何修改 Makefile
所需条件
硬件:
- 3 个 Nordic Semiconductor nRF52840 开发板
- 3 条 USB 转 Micro USB 线,用于连接开发板
- 一台至少有 3 个 USB 端口的 Linux 机器
软件:
- GNU 工具链
- Nordic nRF5x 命令行工具
- Segger J-Link 软件
- OpenThread
- Git
除非另有说明,否则本 Codelab 中的内容已获得 Creative Commons Attribution 3.0 License 许可,代码示例已获得 Apache 2.0 License 许可。
2. 使用入门
完成硬件 Codelab
在开始此 Codelab 之前,您应先完成 使用 nRF52840 开发板和 OpenThread 构建 Thread 网络 Codelab,其中:
- 详细介绍了构建和刷写所需的所有软件
- 介绍如何构建 OpenThread 并将其刷写到 Nordic nRF52840 开发板上
- 演示 Thread 网络的基础知识
本 Codelab 中未详细介绍构建 OpenThread 和刷写开发板所需的任何环境设置,仅介绍了刷写开发板的基本说明。假设您已完成“构建线程网络”Codelab。
Linux 机器
此 Codelab 旨在使用基于 i386 或 x86 的 Linux 机器来刷写所有 Thread 开发板。所有步骤均已在 Ubuntu 14.04.5 LTS (Trusty Tahr) 上进行了测试。
Nordic Semiconductor nRF52840 开发板
此 Codelab 使用三个 nRF52840 PDK 开发板。
安装软件
如需构建和刷写 OpenThread,您需要安装 SEGGER J-Link、nRF5x 命令行工具、ARM GNU 工具链和各种 Linux 软件包。如果您已按照要求完成了“构建线程网络”Codelab,则已安装所需的所有内容。如果没有,请先完成该 Codelab,然后再继续操作,以确保您能够构建 OpenThread 并将其刷写到 nRF52840 开发板。
3. 克隆代码库
OpenThread 附带示例应用代码,可用作此 Codelab 的起点。
克隆 OpenThread Nordic nRF528xx 示例代码库并构建 OpenThread:
$ git clone --recursive https://github.com/openthread/ot-nrf528xx $ cd ot-nrf528xx $ ./script/bootstrap
4. OpenThread API 基础知识
OpenThread 的公共 API 位于 OpenThread 代码库的 ./openthread/include/openthread
中。这些 API 可让您在线程级别和平台级别访问各种 OpenThread 功能,以便在应用中使用:
- OpenThread 实例信息和控制
- IPv6、UDP 和 CoAP 等应用服务
- 网络凭据管理以及“委托人”和“加入者”角色
- 边界路由器管理
- 增强型功能,例如儿童监督和干扰检测
如需了解所有 OpenThread API 的参考信息,请访问 openthread.io/reference。
使用 API
如需使用某个 API,请在某个应用文件中添加其头文件。然后调用所需的函数。
例如,OpenThread 中包含的 CLI 示例应用使用以下 API 标头:
./openthread/examples/apps/cli/main.c
#include <openthread/config.h> #include <openthread/cli.h> #include <openthread/diag.h> #include <openthread/tasklet.h> #include <openthread/platform/logging.h>
OpenThread 实例
在使用 OpenThread API 时,您会经常使用 otInstance
结构。初始化后,此结构表示 OpenThread 库的静态实例,并允许用户进行 OpenThread API 调用。
例如,OpenThread 实例是在 CLI 示例应用的 main()
函数中初始化的:
./openthread/examples/apps/cli/main.c
int main(int argc, char *argv[]) { otInstance *instance ... #if OPENTHREAD_ENABLE_MULTIPLE_INSTANCES // Call to query the buffer size (void)otInstanceInit(NULL, &otInstanceBufferLength); // Call to allocate the buffer otInstanceBuffer = (uint8_t *)malloc(otInstanceBufferLength); assert(otInstanceBuffer); // Initialize OpenThread with the buffer instance = otInstanceInit(otInstanceBuffer, &otInstanceBufferLength); #else instance = otInstanceInitSingle(); #endif ... return 0; }
平台专用函数
如果您想向 OpenThread 附带的某个示例应用中添加平台专用函数,请先在 ./openthread/examples/platforms/openthread-system.h
头文件中声明这些函数,并为所有函数使用 otSys
命名空间。然后,在平台专用源文件中实现它们。通过这种方式进行抽象化后,您可以为其他示例平台使用相同的函数头文件。
例如,我们将用于钩接到 nRF52840 按钮和 LED 的 GPIO 函数必须在 openthread-system.h
中声明。
在首选文本编辑器中打开 ./openthread/examples/platforms/openthread-system.h
文件。
./openthread/examples/platforms/openthread-system.h
操作:添加平台专用 GPIO 函数声明。
在 openthread/instance.h
头文件的 #include
后面添加以下函数声明:
/** * Init LED module. * */ void otSysLedInit(void); void otSysLedSet(uint8_t aLed, bool aOn); void otSysLedToggle(uint8_t aLed); /** * A callback will be called when GPIO interrupts occur. * */ typedef void (*otSysButtonCallback)(otInstance *aInstance); void otSysButtonInit(otSysButtonCallback aCallback); void otSysButtonProcess(otInstance *aInstance);
我们将在下一步中实现这些操作。
请注意,otSysButtonProcess
函数声明使用 otInstance
。这样,应用便可以在需要时在按下按钮时访问 OpenThread 实例的相关信息。这完全取决于应用的需求。如果您在函数实现中不需要它,则可以使用 OpenThread API 中的 OT_UNUSED_VARIABLE
宏来抑制某些工具链中未使用的变量周围的构建错误。我们稍后会看到相关示例。
5. 实现 GPIO 平台抽象
在上一步中,我们介绍了 ./openthread/examples/platforms/openthread-system.h
中可用于 GPIO 的平台专用函数声明。如需访问 nRF52840 开发板上的按钮和 LED,您需要为 nRF52840 平台实现这些函数。在此代码中,您将添加以下函数:
- 初始化 GPIO 引脚和模式
- 控制引脚上的电压
- 启用 GPIO 中断并注册回调
在 ./src/src
目录中,创建一个名为 gpio.c
的新文件。在新文件中,添加以下内容。
./src/src/gpio.c(新文件)
操作:添加了定义。
这些定义可用作 nRF52840 专用值与 OpenThread 应用级别使用的变量之间的抽象。
/** * @file * This file implements the system abstraction for GPIO and GPIOTE. * */ #define BUTTON_GPIO_PORT 0x50000300UL #define BUTTON_PIN 11 // button #1 #define GPIO_LOGIC_HI 0 #define GPIO_LOGIC_LOW 1 #define LED_GPIO_PORT 0x50000300UL #define LED_1_PIN 13 // turn on to indicate leader role #define LED_2_PIN 14 // turn on to indicate router role #define LED_3_PIN 15 // turn on to indicate child role #define LED_4_PIN 16 // turn on to indicate UDP receive
如需详细了解 nRF52840 按钮和 LED,请参阅 Nordic Semiconductor Infocenter。
操作:添加了标头包含信息。
接下来,添加 GPIO 功能所需的头文件包含项。
/* Header for the functions defined here */ #include "openthread-system.h" #include <string.h> /* Header to access an OpenThread instance */ #include <openthread/instance.h> /* Headers for lower-level nRF52840 functions */ #include "platform-nrf5.h" #include "hal/nrf_gpio.h" #include "hal/nrf_gpiote.h" #include "nrfx/drivers/include/nrfx_gpiote.h"
措施:为按钮 1 添加回调和中断函数。
接下来,添加以下代码。in_pin1_handler
函数是在初始化按钮按压功能时注册的回调(此文件稍后部分)。
请注意此回调如何使用 OT_UNUSED_VARIABLE
宏,因为传递给 in_pin1_handler
的变量实际上并未在该函数中使用。
/* Declaring callback function for button 1. */ static otSysButtonCallback sButtonHandler; static bool sButtonPressed; /** * @brief Function to receive interrupt and call back function * set by the application for button 1. * */ static void in_pin1_handler(uint32_t pin, nrf_gpiote_polarity_t action) { OT_UNUSED_VARIABLE(pin); OT_UNUSED_VARIABLE(action); sButtonPressed = true; }
操作:添加用于配置 LED 的函数。
添加以下代码,以便在初始化期间配置所有 LED 的模式和状态。
/** * @brief Function for configuring: PIN_IN pin for input, PIN_OUT pin for output, * and configures GPIOTE to give an interrupt on pin change. */ void otSysLedInit(void) { /* Configure GPIO mode: output */ nrf_gpio_cfg_output(LED_1_PIN); nrf_gpio_cfg_output(LED_2_PIN); nrf_gpio_cfg_output(LED_3_PIN); nrf_gpio_cfg_output(LED_4_PIN); /* Clear all output first */ nrf_gpio_pin_write(LED_1_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_2_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_3_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_4_PIN, GPIO_LOGIC_LOW); /* Initialize gpiote for button(s) input. Button event handlers are set in the application (main.c) */ ret_code_t err_code; err_code = nrfx_gpiote_init(); APP_ERROR_CHECK(err_code); }
操作:添加用于设置 LED 模式的函数。
当设备的角色发生变化时,系统会使用此函数。
/** * @brief Function to set the mode of an LED. */ void otSysLedSet(uint8_t aLed, bool aOn) { switch (aLed) { case 1: nrf_gpio_pin_write(LED_1_PIN, (aOn == GPIO_LOGIC_HI)); break; case 2: nrf_gpio_pin_write(LED_2_PIN, (aOn == GPIO_LOGIC_HI)); break; case 3: nrf_gpio_pin_write(LED_3_PIN, (aOn == GPIO_LOGIC_HI)); break; case 4: nrf_gpio_pin_write(LED_4_PIN, (aOn == GPIO_LOGIC_HI)); break; } }
操作:添加用于切换 LED 模式的函数。
此函数将用于在设备收到多播 UDP 消息时切换 LED4。
/** * @brief Function to toggle the mode of an LED. */ void otSysLedToggle(uint8_t aLed) { switch (aLed) { case 1: nrf_gpio_pin_toggle(LED_1_PIN); break; case 2: nrf_gpio_pin_toggle(LED_2_PIN); break; case 3: nrf_gpio_pin_toggle(LED_3_PIN); break; case 4: nrf_gpio_pin_toggle(LED_4_PIN); break; } }
操作:添加用于初始化和处理按钮按压的函数。
第一个函数会初始化板以响应按钮按下,第二个函数会在按下按钮 1 时发送多播 UDP 消息。
/** * @brief Function to initialize the button. */ void otSysButtonInit(otSysButtonCallback aCallback) { nrfx_gpiote_in_config_t in_config = NRFX_GPIOTE_CONFIG_IN_SENSE_LOTOHI(true); in_config.pull = NRF_GPIO_PIN_PULLUP; ret_code_t err_code; err_code = nrfx_gpiote_in_init(BUTTON_PIN, &in_config, in_pin1_handler); APP_ERROR_CHECK(err_code); sButtonHandler = aCallback; sButtonPressed = false; nrfx_gpiote_in_event_enable(BUTTON_PIN, true); } void otSysButtonProcess(otInstance *aInstance) { if (sButtonPressed) { sButtonPressed = false; sButtonHandler(aInstance); } }
操作:保存并关闭 gpio.c
文件。
6. API:响应设备角色变更
在我们的应用中,我们希望不同的 LED 指示灯根据设备角色亮起。我们来跟踪以下角色:主副本、路由器、端设备。我们可以将它们分配给 LED,如下所示:
- LED1 = 主副本
- LED2 = 路由器
- LED3 = 终端设备
如需启用此功能,应用需要知道设备角色何时发生变化,以及如何相应地开启正确的 LED。我们将在第一部分使用 OpenThread 实例,在第二部分使用 GPIO 平台抽象。
在首选文本编辑器中打开 ./openthread/examples/apps/cli/main.c
文件。
./openthread/examples/apps/cli/main.c
操作:添加了标头包含信息。
在 main.c
文件的“includes”部分,添加角色更改功能所需的 API 头文件。
#include <openthread/instance.h> #include <openthread/thread.h> #include <openthread/thread_ftd.h>
操作:为 OpenThread 实例状态更改添加了处理脚本函数声明。
将此声明添加到 main.c
中,放在头文件包含代码之后且任何 #if
语句之前。此函数将在主应用之后定义。
void handleNetifStateChanged(uint32_t aFlags, void *aContext);
操作:为状态更改处理脚本函数添加回调注册。
在 main.c
中,将此函数添加到 main()
函数的 otAppCliInit
调用后面。此回调注册会告知 OpenThread 在 OpenThread 实例状态发生变化时调用 handleNetifStateChange
函数。
/* Register Thread state change handler */ otSetStateChangedCallback(instance, handleNetifStateChanged, instance);
操作:添加状态更改实现。
在 main.c
中,在 main()
函数后面,实现 handleNetifStateChanged
函数。此函数会检查 OpenThread 实例的 OT_CHANGED_THREAD_ROLE
标志,如果该标志已更改,则根据需要开启/关闭 LED。
void handleNetifStateChanged(uint32_t aFlags, void *aContext) { if ((aFlags & OT_CHANGED_THREAD_ROLE) != 0) { otDeviceRole changedRole = otThreadGetDeviceRole(aContext); switch (changedRole) { case OT_DEVICE_ROLE_LEADER: otSysLedSet(1, true); otSysLedSet(2, false); otSysLedSet(3, false); break; case OT_DEVICE_ROLE_ROUTER: otSysLedSet(1, false); otSysLedSet(2, true); otSysLedSet(3, false); break; case OT_DEVICE_ROLE_CHILD: otSysLedSet(1, false); otSysLedSet(2, false); otSysLedSet(3, true); break; case OT_DEVICE_ROLE_DETACHED: case OT_DEVICE_ROLE_DISABLED: /* Clear LED4 if Thread is not enabled. */ otSysLedSet(4, false); break; } } }
7. API:使用多播开启 LED 灯
在我们的应用中,我们还希望在某个开发板上按下 Button1 时,向网络中的所有其他设备发送 UDP 消息。为了确认收到消息,我们会在其他开发板上切换 LED4。
如需启用此功能,应用需要满足以下条件:
- 在启动时初始化 UDP 连接
- 能够向网状局域多播地址发送 UDP 消息
- 处理传入的 UDP 消息
- 在响应传入的 UDP 消息时切换 LED4
在首选文本编辑器中打开 ./openthread/examples/apps/cli/main.c
文件。
./openthread/examples/apps/cli/main.c
操作:添加了标头包含信息。
在 main.c
文件顶部的“includes”部分,添加多播 UDP 功能所需的 API 头文件。
#include <string.h> #include <openthread/message.h> #include <openthread/udp.h> #include "utils/code_utils.h"
code_utils.h
头文件用于 otEXPECT
和 otEXPECT_ACTION
宏,用于验证运行时条件并妥善处理错误。
操作:添加定义和常量:
在 main.c
文件中,在“includes”部分之后、任何 #if
语句之前,添加 UDP 专用常量和定义:
#define UDP_PORT 1212 static const char UDP_DEST_ADDR[] = "ff03::1"; static const char UDP_PAYLOAD[] = "Hello OpenThread World!";
ff03::1
是网状局域多播地址。发送到此地址的所有消息都会发送到网络中的所有 Full Thread 设备。如需详细了解 OpenThread 中的多播支持,请参阅 openthread.io 上的多播。
操作:添加函数声明。
在 main.c
文件中,在 otTaskletsSignalPending
定义后面和 main()
函数前面,添加 UDP 专用函数以及表示 UDP 套接字的静态变量:
static void initUdp(otInstance *aInstance); static void sendUdp(otInstance *aInstance); static void handleButtonInterrupt(otInstance *aInstance); void handleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo); static otUdpSocket sUdpSocket;
操作:添加了用于初始化 GPIO LED 和按钮的调用。
在 main.c
中,将以下函数调用添加到 main()
函数的 otSetStateChangedCallback
调用后面。这些函数会初始化 GPIO 和 GPIOTE 引脚,并设置按钮处理脚本以处理按钮按压事件。
/* init GPIO LEDs and button */ otSysLedInit(); otSysButtonInit(handleButtonInterrupt);
措施:添加 UDP 初始化调用。
在 main.c
中,将此函数添加到 main()
函数中,并将其添加到您刚刚添加的 otSysButtonInit
调用后面:
initUdp(instance);
此调用可确保在应用启动时初始化 UDP 套接字。否则,设备将无法发送或接收 UDP 消息。
操作:添加了用于处理 GPIO 按钮事件的调用。
在 main.c
中,在 while
循环中的 otSysProcessDrivers
调用后面,将此函数调用添加到 main()
函数。此函数在 gpio.c
中声明,用于检查是否按下了按钮,如果按下了,则调用在上一步中设置的处理脚本 (handleButtonInterrupt
)。
otSysButtonProcess(instance);
操作:实现按钮中断处理程序。
在 main.c
中,在您在上一步中添加的 handleNetifStateChanged
函数后面添加 handleButtonInterrupt
函数的实现:
/** * Function to handle button push event */ void handleButtonInterrupt(otInstance *aInstance) { sendUdp(aInstance); }
措施:实现 UDP 初始化。
在 main.c
中,在您刚刚添加的 handleButtonInterrupt
函数后面添加 initUdp
函数的实现:
/** * Initialize UDP socket */ void initUdp(otInstance *aInstance) { otSockAddr listenSockAddr; memset(&sUdpSocket, 0, sizeof(sUdpSocket)); memset(&listenSockAddr, 0, sizeof(listenSockAddr)); listenSockAddr.mPort = UDP_PORT; otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, aInstance); otUdpBind(aInstance, &sUdpSocket, &listenSockAddr, OT_NETIF_THREAD); }
UDP_PORT
是您之前定义的端口 (1212)。otUdpOpen
函数会打开套接字,并注册一个回调函数 (handleUdpReceive
),以便在收到 UDP 消息时调用该函数。otUdpBind
通过传递 OT_NETIF_THREAD
将套接字绑定到 Thread 网络接口。如需了解其他网络接口选项,请参阅 UDP API 参考文档中的 otNetifIdentifier
枚举。
措施:实现 UDP 消息传递。
在 main.c
中,在您刚刚添加的 initUdp
函数后面添加 sendUdp
函数的实现:
/** * Send a UDP datagram */ void sendUdp(otInstance *aInstance) { otError error = OT_ERROR_NONE; otMessage * message; otMessageInfo messageInfo; otIp6Address destinationAddr; memset(&messageInfo, 0, sizeof(messageInfo)); otIp6AddressFromString(UDP_DEST_ADDR, &destinationAddr); messageInfo.mPeerAddr = destinationAddr; messageInfo.mPeerPort = UDP_PORT; message = otUdpNewMessage(aInstance, NULL); otEXPECT_ACTION(message != NULL, error = OT_ERROR_NO_BUFS); error = otMessageAppend(message, UDP_PAYLOAD, sizeof(UDP_PAYLOAD)); otEXPECT(error == OT_ERROR_NONE); error = otUdpSend(aInstance, &sUdpSocket, message, &messageInfo); exit: if (error != OT_ERROR_NONE && message != NULL) { otMessageFree(message); } }
请注意 otEXPECT
和 otEXPECT_ACTION
宏。这些操作可确保 UDP 消息有效且在缓冲区中正确分配。如果不正确,该函数会跳转到 exit
块(在该块中释放缓冲区),以妥善处理错误。
如需详细了解用于初始化 UDP 的函数,请参阅 openthread.io 上的 IPv6 和 UDP 参考文档。
措施:实现 UDP 消息处理。
在 main.c
中,在您刚刚添加的 sendUdp
函数后面添加 handleUdpReceive
函数的实现。此函数只是切换 LED4。
/** * Function to handle UDP datagrams received on the listening socket */ void handleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo) { OT_UNUSED_VARIABLE(aContext); OT_UNUSED_VARIABLE(aMessage); OT_UNUSED_VARIABLE(aMessageInfo); otSysLedToggle(4); }
8. API:配置 Thread 网络
为方便演示,我们希望设备在开机后立即启动 Thread 并加入网络。为此,我们将使用 otOperationalDataset
结构。此结构包含将 Thread 网络凭据传输到设备所需的所有参数。
使用此结构会替换 OpenThread 中内置的网络默认设置,以提高应用的安全性,并将网络中的 Thread 节点限制为仅运行应用的节点。
再次在首选文本编辑器中打开 ./openthread/examples/apps/cli/main.c
文件。
./openthread/examples/apps/cli/main.c
操作:添加了头文件包含。
在 main.c
文件顶部的“includes”部分中,添加配置 Thread 网络所需的 API 头文件:
#include <openthread/dataset_ftd.h>
操作:添加用于设置网络配置的函数声明。
将此声明添加到 main.c
中,放在头文件包含代码之后且任何 #if
语句之前。此函数将在主应用函数后定义。
static void setNetworkConfiguration(otInstance *aInstance);
操作:添加网络配置调用。
在 main.c
中,将此函数调用添加到 main()
函数的 otSetStateChangedCallback
调用后面。此函数用于配置线程网络数据集。
/* Override default network credentials */ setNetworkConfiguration(instance);
操作:添加了用于启用 Thread 网络接口和堆栈的调用。
在 main.c
中,将以下函数调用添加到 main()
函数的 otSysButtonInit
调用后面。
/* Start the Thread network interface (CLI cmd > ifconfig up) */ otIp6SetEnabled(instance, true); /* Start the Thread stack (CLI cmd > thread start) */ otThreadSetEnabled(instance, true);
措施:实现 Thread 网络配置。
在 main.c
中,在 main()
函数后面添加 setNetworkConfiguration
函数的实现:
/** * Override default network settings, such as panid, so the devices can join a network */ void setNetworkConfiguration(otInstance *aInstance) { static char aNetworkName[] = "OTCodelab"; otOperationalDataset aDataset; memset(&aDataset, 0, sizeof(otOperationalDataset)); /* * Fields that can be configured in otOperationDataset to override defaults: * Network Name, Mesh Local Prefix, Extended PAN ID, PAN ID, Delay Timer, * Channel, Channel Mask Page 0, Network Key, PSKc, Security Policy */ aDataset.mActiveTimestamp.mSeconds = 1; aDataset.mActiveTimestamp.mTicks = 0; aDataset.mActiveTimestamp.mAuthoritative = false; aDataset.mComponents.mIsActiveTimestampPresent = true; /* Set Channel to 15 */ aDataset.mChannel = 15; aDataset.mComponents.mIsChannelPresent = true; /* Set Pan ID to 2222 */ aDataset.mPanId = (otPanId)0x2222; aDataset.mComponents.mIsPanIdPresent = true; /* Set Extended Pan ID to C0DE1AB5C0DE1AB5 */ uint8_t extPanId[OT_EXT_PAN_ID_SIZE] = {0xC0, 0xDE, 0x1A, 0xB5, 0xC0, 0xDE, 0x1A, 0xB5}; memcpy(aDataset.mExtendedPanId.m8, extPanId, sizeof(aDataset.mExtendedPanId)); aDataset.mComponents.mIsExtendedPanIdPresent = true; /* Set network key to 1234C0DE1AB51234C0DE1AB51234C0DE */ uint8_t key[OT_NETWORK_KEY_SIZE] = {0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE}; memcpy(aDataset.mNetworkKey.m8, key, sizeof(aDataset.mNetworkKey)); aDataset.mComponents.mIsNetworkKeyPresent = true; /* Set Network Name to OTCodelab */ size_t length = strlen(aNetworkName); assert(length <= OT_NETWORK_NAME_MAX_SIZE); memcpy(aDataset.mNetworkName.m8, aNetworkName, length); aDataset.mComponents.mIsNetworkNamePresent = true; otDatasetSetActive(aInstance, &aDataset); /* Set the router selection jitter to override the 2 minute default. CLI cmd > routerselectionjitter 20 Warning: For demo purposes only - not to be used in a real product */ uint8_t jitterValue = 20; otThreadSetRouterSelectionJitter(aInstance, jitterValue); }
如该函数中所详述,我们为此应用使用的 Thread 网络参数如下:
- 频道 = 15
- PAN ID = 0x2222
- 扩展 PAN ID = C0DE1AB5C0DE1AB5
- 广告联盟键 = 1234C0DE1AB51234C0DE1AB51234C0DE
- 广告网络名称 = OTCodelab
此外,我们还会在此处减少路由器选择抖动,以便设备更快地更改角色(出于演示目的)。请注意,只有当节点是 FTD(完整线程设备)时,才会执行此操作。有关详情,请参阅下一步。
9. API:受限函数
OpenThread 的某些 API 会修改一些设置,这些设置应仅出于演示或测试目的而修改。请勿在使用 OpenThread 的应用的正式版部署中使用这些 API。
例如,otThreadSetRouterSelectionJitter
函数会调整终端设备将自己提升为路由器所需的时间(以秒为单位)。根据线程规范,此值的默认值为 120。为方便在此 Codelab 中使用,我们将其更改为 20,这样您就不必等待很长时间,线程节点就能更改角色。
注意:MTD 设备不会成为路由器,MTD build 也不支持 otThreadSetRouterSelectionJitter
等函数。稍后,我们需要指定 CMake 选项 -DOT_MTD=OFF
,否则会遇到构建失败问题。
您可以通过查看 otThreadSetRouterSelectionJitter
函数定义(包含在 OPENTHREAD_FTD
的预处理器指令中)来确认这一点:
./openthread/src/core/api/thread_ftd_api.cpp
#if OPENTHREAD_FTD #include <openthread/thread_ftd.h> ... void otThreadSetRouterSelectionJitter(otInstance *aInstance, uint8_t aRouterJitter) { Instance &instance = *static_cast<Instance *>(aInstance); instance.GetThreadNetif().GetMle().SetRouterSelectionJitter(aRouterJitter); } ... #endif // OPENTHREAD_FTD
10. CMake 更新
在构建应用之前,需要对三个 CMake 文件进行一些细微更新。构建系统会使用这些信息来编译和关联您的应用。
./third_party/NordicSemiconductor/CMakeLists.txt
现在,向 NordicSemiconductor CMakeLists.txt
添加一些标志,以确保在应用中定义 GPIO 函数。
操作:向 CMakeLists.txt
文件添加标志。
在首选文本编辑器中打开 ./third_party/NordicSemiconductor/CMakeLists.txt
,然后在 COMMON_FLAG
部分中添加以下代码行。
... set(COMMON_FLAG -DSPIS_ENABLED=1 -DSPIS0_ENABLED=1 -DNRFX_SPIS_ENABLED=1 -DNRFX_SPIS0_ENABLED=1 ... # Defined in ./third_party/NordicSemiconductor/nrfx/templates/nRF52840/nrfx_config.h -DGPIOTE_ENABLED=1 -DGPIOTE_CONFIG_IRQ_PRIORITY=7 -DGPIOTE_CONFIG_NUM_OF_LOW_POWER_EVENTS=1 ) ...
./src/CMakeLists.txt
修改 ./src/CMakeLists.txt
文件以添加新的 gpio.c
源文件:
操作:将 gpio 源添加到 ./src/CMakeLists.txt
文件。
在首选文本编辑器中打开 ./src/CMakeLists.txt
,然后将该文件添加到 NRF_COMM_SOURCES
部分。
... set(NRF_COMM_SOURCES ... src/gpio.c ... ) ...
./third_party/NordicSemiconductor/CMakeLists.txt
最后,将 nrfx_gpiote.c
驱动程序文件添加到 NordicSemiconductor CMakeLists.txt
文件,以便将其包含在 Nordic 驱动程序的库 build 中。
操作:将 gpio 驱动程序添加到 NordicSemiconductor CMakeLists.txt
文件。
在首选文本编辑器中打开 ./third_party/NordicSemiconductor/CMakeLists.txt
,然后将该文件添加到 COMMON_SOURCES
部分。
... set(COMMON_SOURCES ... nrfx/drivers/src/nrfx_gpiote.c ... ) ...
11. 设置设备
完成所有代码更新后,您就可以构建应用并将其刷写到三个 Nordic nRF52840 开发板上了。每台设备都将用作完整 Thread 设备 (FTD)。
构建 OpenThread
为 nRF52840 平台构建 OpenThread FTD 二进制文件。
$ cd ~/ot-nrf528xx $ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF
前往包含 OpenThread FTD CLI 二进制文件的目录,并使用 ARM 嵌入式工具链将其转换为十六进制格式:
$ cd build/bin $ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex
刷写开发板
将 ot-cli-ftd.hex
文件刷写到每个 nRF52840 开发板。
将 USB 线连接到 nRF52840 开发板外部电源引脚旁边的 Micro-USB 调试端口,然后将其插入 Linux 计算机。正确设置后,LED5 处于亮起状态。
与之前一样,记下 nRF52840 开发板的序列号:
前往 nRFx 命令行工具的位置,然后使用开发板的序列号将 OpenThread CLI FTD 十六进制文件刷写到 nRF52840 开发板:
$ cd ~/nrfjprog $ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \ ~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset
LED5 在闪烁期间会短暂关闭。成功后,系统会生成以下输出:
Parsing hex file. Erasing user available code and UICR flash areas. Applying system reset. Checking that the area to write is not protected. Programing device. Applying system reset. Run.
对另外两个开发板重复此“刷写开发板”步骤。每个开发板都应以相同的方式连接到 Linux 机器,并且刷写命令也相同,除了开发板的序列号外。请务必在
nrfjprog
刷写命令。
如果成功,每个开发板上的 LED1、LED2 或 LED3 都会亮起。您甚至可能会在闪烁后不久看到亮起的 LED 指示灯从 3 切换为 2(或 2 切换为 1)(设备角色更改功能)。
12. 应用功能
现在,所有三个 nRF52840 开发板都已开机并运行我们的 OpenThread 应用。如前所述,此应用具有两项主要功能。
设备角色指示器
每个开发板上的亮起 LED 灯反映了 Thread 节点的当前角色:
- LED1 = 主副本
- LED2 = 路由器
- LED3 = 终端设备
随着角色的变化,LED 灯的亮起情况也会随之变化。您应该已经在每台设备开机后的 20 秒内看到一两个开发板上发生了这些变化。
UDP 多播
当板子上的 Button1 被按下时,系统会向网状局域多播地址发送 UDP 消息,其中包含 Thread 网络中的所有其他节点。收到此消息后,所有其他开发板上的 LED4 都会切换开启或关闭状态。每个开发板上的 LED4 都会保持开启或关闭状态,直到收到另一个 UDP 消息。
13. 演示:观察设备角色更改
您刷写的设备是一种特定类型的完整 Thread 设备 (FTD),称为路由器可用端设备 (REED)。这意味着它们可以充当路由器或端末设备,并且可以将自己从端末设备提升为路由器。
Thread 最多可支持 32 个路由器,但会尝试将路由器数量保持在 16 到 23 之间。如果 REED 作为端点设备附加,并且路由器数量低于 16 个,则会自动将自身提升为路由器。此更改应在您在应用中为 otThreadSetRouterSelectionJitter
值设置的秒数(20 秒)内随机发生。
每个 Thread 网络还具有一个主路由器,它负责管理 Thread 网络中的一组路由器。所有设备都处于开启状态后,20 秒后其中一个设备应为主设备(LED1 指示灯亮起),另外两个设备应为路由器(LED2 指示灯亮起)。
移除主管
如果主路由器从 Thread 网络中移除,其他路由器会将自己提升为主路由器,以确保网络仍有主路由器。
使用电源开关关闭排行榜(LED1 指示灯亮起)。等待大约 20 秒。在剩余的两个开发板中,LED2(路由器)将关闭,LED1(主副本)将亮起。此设备现在是 Thread 网络的主设备。
重新启用原始排行榜。它应自动以端点设备的身份重新加入 Thread 网络(LED3 指示灯亮起)。在 20 秒内(路由器选择抖动时间),它会将自身提升为路由器(LED2 亮起)。
重置开发板
关闭所有三个开发板,然后重新打开并观察 LED 指示灯。首先开机的开发板应以主设备角色启动(LED1 指示灯亮起)- Thread 网络中的第一个路由器会自动成为主设备。
另外两个开发板最初会作为端点设备连接到网络(LED3 亮起),但应在 20 秒内将自己提升为路由器(LED2 亮起)。
网络分区
如果开发板未获得足够的电源,或者开发板之间的无线连接较弱,Thread 网络可能会拆分为多个分区,并且您可能会有多个设备显示为主设备。
线程是自修复的,因此分区最终应合并回一个具有一个主副本的分区。
14. 演示:发送 UDP 多播
如果从上一个练习继续,任何设备上的 LED4 都不会亮起。
选择任意一个开发板,然后按 Button1。运行该应用的 Thread 网络中的所有其他开发板上的 LED4 都应切换状态。如果您是从上一个练习继续,这些功能现在应该处于开启状态。
再次按同一开发板的按钮 1。所有其他开发板上的 LED4 应再次切换。
按其他开发板上的按钮 1,观察 LED4 在其他开发板上的切换情况。按下 LED4 当前处于开启状态的某个开发板上的按钮 1。LED4 会继续亮起,但其他 LED 会切换为亮起状态。
网络分区
如果您的董事会已分区,并且其中有多个主导董事会,则多播消息的结果会因董事会而异。如果您按下已分区的开发板(因此是分区 Thread 网络的唯一成员)上的按钮 1,其他开发板上的 LED4 不会亮起。如果发生这种情况,请重置开发板;理想情况下,它们会重新组成单个线程网络,并且 UDP 消息传递应该会正常运行。
15. 恭喜!
您已创建一个使用 OpenThread API 的应用!
您现在了解了:
- 如何对 Nordic nRF52840 开发板上的按钮和 LED 进行编程
- 如何使用常见的 OpenThread API 和
otInstance
类 - 如何监控 OpenThread 状态变化并对其做出响应
- 如何向 Thread 网络中的所有设备发送 UDP 消息
- 如何修改 Makefile
后续步骤
在此 Codelab 的基础上,尝试做以下练习:
- 修改 GPIO 模块以使用 GPIO 引脚(而非板载 LED),并连接会根据路由器角色更改颜色的外部 RGB LED
- 为其他示例平台添加 GPIO 支持
- 请改用 Router/Leader API 来定位和 ping 单个设备,而不是使用多播在按下按钮时 ping 所有设备
- 使用 OpenThread 边界路由器将网状网络连接到互联网,并从 Thread 网络外部广播它们以点亮 LED
深入阅读
请访问 openthread.io 和 GitHub,获取各种 OpenThread 资源,包括:
- 支持的平台 - 了解支持 OpenThread 的所有平台
- 构建 OpenThread - 详细了解如何构建和配置 OpenThread
- 线程入门指南 - 有关线程概念的绝佳参考文档
参考: