1. 簡介
Nest 發布的 OpenThread 是 Thread® 網路通訊協定的開放原始碼實作項目。Nest 推出 OpenThread,讓開發人員廣泛使用 Nest 產品中的技術,加快開發智慧聯網家庭產品。
Thread 規格定義了以 IPv6 為基礎的無線裝置間通訊協定,可用於家用應用程式,提供可靠、安全且耗電量低的通訊機制。OpenThread 實作所有 Thread 網路層,包括 IPv6、6LoWPAN、IEEE 802.15.4 (含 MAC 安全性)、Mesh Link 建立和 Mesh 路由。
在本程式碼研究室中,您將使用 OpenThread API 啟動 Thread 網路、監控裝置角色的變更並做出回應,以及傳送 UDP 訊息,並將這些動作與實際硬體上的按鈕和 LED 連結。
課程內容
- 如何在 Nordic nRF52840 開發板上編寫按鈕和 LED 程式
- 如何使用常見的 OpenThread API 和
otInstance
類別 - 如何監控及回應 OpenThread 狀態變更
- 如何將 UDP 訊息傳送至 Thread 網路中的所有裝置
- 如何修改 Makefile
軟硬體需求
硬體:
- 3 個 Nordic Semiconductor nRF52840 開發板
- 3 條 USB 轉 Micro-USB 傳輸線,用於連接電路板
- 至少有 3 個 USB 連接埠的 Linux 電腦
軟體:
- GNU 工具鍊
- Nordic nRF5x 指令列工具
- Segger J-Link 軟體
- OpenThread
- Git
除非另有註明,否則本程式碼研究室的內容採用 Creative Commons 姓名標示 3.0 授權,程式碼範例則採用 Apache 2.0 授權。
2. 開始使用
完成硬體程式碼研究室
開始本程式碼研究室之前,請先完成使用 nRF52840 板和 OpenThread 建構 Thread 網路程式碼研究室,其中包含:
- 詳細說明建構和閃燈作業所需的所有軟體
- 說明如何建構 OpenThread,並在 Nordic nRF52840 電路板上進行閃燈
- 示範 Thread 網路的基本概念
本程式碼研究室未詳細說明建構 OpenThread 和刷新電路板所需的環境設定,僅提供刷新電路板的基本操作說明。本文假設您已完成「建構 Thread 網路」程式碼研究室。
Linux 機器
本程式碼研究室的設計目的是使用 i386 或 x86 架構的 Linux 機器,對所有 Thread 開發板進行閃燈作業。所有步驟均已在 Ubuntu 14.04.5 LTS (Trusty Tahr) 上測試。
Nordic Semiconductor nRF52840 板卡
本程式碼研究室使用三個 nRF52840 PDK 板。
安裝軟體
如要建構及刷新 OpenThread,您必須安裝 SEGGER J-Link、nRF5x 指令列工具、ARM GNU Toolchain 和各種 Linux 套件。如果您已按照要求完成建立 Thread 網路程式碼研究室,就已安裝所有必要項目。如果沒有,請先完成該程式碼研究室,再繼續操作,確保您可以建構並將 OpenThread 閃記到 nRF52840 開發板。
3. 複製存放區
OpenThread 提供應用程式程式碼範例,可做為本程式碼研究室的起點。
複製 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"
行動:為 Button 1 新增回呼和中斷函式。
接著加入以下程式碼。in_pin1_handler
函式是按鈕按下功能初始化時註冊的回呼 (稍後會在本檔案中說明)。
請注意,由於函式中並未實際使用傳遞至 in_pin1_handler
的變數,因此這個回呼會使用 OT_UNUSED_VARIABLE
巨集。
/* 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
檔案的包含區段中,新增角色變更功能所需的 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
檔案頂端的包含區段中,新增多播 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
檔案中,在包含區段之後和任何 #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
函式會開啟 Socket,並註冊回呼函式 (handleUdpReceive
),用於接收 UDP 訊息。otUdpBind
會傳遞 OT_NETIF_THREAD
,將 Socket 繫結至 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
呼叫後方。這個函式會設定 Thread 網路資料集。
/* 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
- 擴充永久帳號 ID = C0DE1AB5C0DE1AB5
- 網路金鑰 = 1234C0DE1AB51234C0DE1AB51234C0DE
- 網路名稱 = OTCodelab
此外,我們也會在這裡減少 Router Selection Jitter,讓裝置更快變更角色,以利於示範。請注意,只有在節點為 FTD (Full Thread Device) 時,才會執行這項操作。請在下一個步驟中進一步瞭解。
9. API:受限制的函式
部分 OpenThread API 會修改設定,但這些設定只應用於示範或測試。請勿在使用 OpenThread 的應用程式正式部署中使用這些 API。
舉例來說,otThreadSetRouterSelectionJitter
函式會調整終端裝置將自身提升為路由器所需的時間 (以秒為單位)。根據 Thread 規格,這個值的預設值為 120。為了讓本程式碼研究室更容易使用,我們會將其變更為 20,這樣您就不必等待太久,就能讓 Thread 節點變更角色。
注意:MTD 裝置不會成為路由器,且 MTD 版本不支援 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 驅動程式的程式庫版本。
行動:將 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 Embedded Toolchain 將其轉換為十六進位格式:
$ 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 16 進位檔刷新至 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. 示範:觀察裝置角色變更
你刷過韌體的裝置是一種稱為「路由器適用終端裝置」(REED) 的完整 Thread 裝置 (FTD)。這表示這些裝置可做為路由器或終端裝置,並可將自己從終端裝置升級為路由器。
執行緒最多可支援 32 個 Router,但會嘗試將 Router 的數量維持在 16 到 23 之間。如果 REED 以終端裝置的形式連線,且路由器數量低於 16,REED 就會自動升級為路由器。這個變更應在應用程式中設定 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 應再次切換。
按下其他電路板上的 Button1,觀察 LED4 在其他電路板上的切換情形。在 LED4 目前亮起的板子上按下 Button1。LED4 會繼續亮起,但其他板會切換為亮起。
網路分區
如果您的看板已劃分,且其中有多個領袖,則多播訊息的結果會因看板而異。如果您在已分割的板子上按下 Button1 (因此是分割 Thread 網路的唯一成員),其他板上的 LED4 就不會亮起。如果發生這種情況,請重設板子,理想情況下,它們會重新建立單一 Thread 網路,UDP 訊息也應該能正常運作。
15. 恭喜!
您已建立使用 OpenThread API 的應用程式!
您現在知道:
- 如何在 Nordic nRF52840 開發板上編寫按鈕和 LED 程式
- 如何使用常見的 OpenThread API 和
otInstance
類別 - 如何監控及回應 OpenThread 狀態變更
- 如何將 UDP 訊息傳送至 Thread 網路中的所有裝置
- 如何修改 Makefile
後續步驟
請根據本程式碼研究室的內容,嘗試下列練習:
- 修改 GPIO 模組,使用 GPIO 針腳而非板載 LED,並連接外部 RGB LED,根據路由器角色變更顏色
- 為其他範例平台新增 GPIO 支援
- 不要使用多播功能,透過按下按鈕來 ping 所有裝置,而是使用Router/Leader API 來定位及 ping 個別裝置
- 使用 OpenThread 邊界路由器將網狀網路連上網際網路,並從 Thread 網路外部進行多點傳送,以便點亮 LED 燈
延伸閱讀
請造訪 openthread.io 和 GitHub,取得各種 OpenThread 資源,包括:
- 支援的平台:查看支援 OpenThread 的所有平台
- 建構 OpenThread:進一步瞭解如何建構及設定 OpenThread
- Thread Primer:有關執行緒概念的絕佳參考資料
參考資料: