前言
很久沒有更新技術(shù)博客了预吆。最近一年都在做低功耗藍牙和物聯(lián)網(wǎng)等相關(guān)的事情薄嫡,想把一些經(jīng)驗和心得寫成博客分享出來,也算對自己工作的一個總結(jié)捡鱼。開一個系列講解NXP的低功耗藍牙SDK開發(fā)的方方面面八回,目前NXP官方僅有一個英文版的《BLE Application Developer Guide》介紹SDK的使用,希望這個系列文章能作為官方文檔的補充驾诈,幫助到正在使用和將要使用NXP藍牙的芯片的朋友們缠诅。
本系列假設(shè)用戶已經(jīng)具備了基本的BLE概念,如Profile乍迄、GAP管引、GATT、HCI闯两、LL等各個層次的職責(zé)褥伴,廣播與連接的行為,基于GATT數(shù)據(jù)傳輸?shù)姆绞降妊恰H绻x者對這個概念還不熟悉重慢,可以閱讀《Getting Started with Bluetooth Low Energy》一書的第1至3章共75頁內(nèi)容,利用業(yè)余時間在2-3天內(nèi)可以輕松完成逊躁。
NXP MCUXpresso SDK
MCUXpresso SDK是NXP面向MCU市場推出的一套完整的軟件開發(fā)套件似踱,支持Kinetis, LPC, i.MX RT等通用微控制器以及QN和JN系列的無線連接MCU。SDK包含了芯片硬件抽象層稽煤,外設(shè)驅(qū)動庫核芽,多個中間件庫(根據(jù)芯片功能的不同包含不同的中間件,比如帶Ethernet外設(shè)的MCU則會有l(wèi)wip網(wǎng)絡(luò)協(xié)議棧中間件)酵熙,以及豐富的示例代碼轧简。
目前在NXP面向物聯(lián)網(wǎng)和可穿戴設(shè)備數(shù)據(jù)傳輸應(yīng)用的BLE SoC芯片中,除QN902x使用原有QN SDK外绿店,其他都采用了MCUXpresso SDK和NXP自研的Bluetooth協(xié)議棧吉懊,呈現(xiàn)給用戶的編程接口完全一致。無論是開發(fā)超低功耗的QN9080假勿,支持Zigbee/Thread/BLE多模的KW41借嗽,還是面向汽車應(yīng)用的KW36,對固件工程師來說转培,掌握了一款芯片的開發(fā)恶导,拿下其他的也就是輕而易舉之事。
對于這些低功耗藍牙MCU浸须,BLE協(xié)議棧是作為MCUXpresso SDK的一個中間件而存在的惨寿。在用戶解壓縮SDK包后邦泄,在./middleware/wireless/bluetooth_x.y.z下可以找到協(xié)議棧的代碼和庫文件。低功耗藍牙應(yīng)用將包含這個目錄下的文件裂垦,以使用協(xié)議棧提供的API和服務(wù)顺囊。所有低功耗藍牙的示例代碼可以再./boards/<specific board>/wireless_examples/bluetooth目錄下找到。同時蕉拢,低功耗藍牙應(yīng)用與通用MCU SDK還有一點差異特碳,它需要一些額外的通用組件作為支撐,如OS環(huán)境抽象晕换、非易失存儲午乓、隊列管理,低功耗管理等闸准,因此多引入了一個connectivity framework的中間件層益愈。在./middleware/wireless/framework_x.y.z下可以找到framework包含的所有功能模塊源代碼。下圖展示了SDK的文件結(jié)構(gòu)中與低功耗藍牙相關(guān)的目錄夷家。
如果基于MCUXpresso IDE開發(fā)蒸其,通常會導(dǎo)出某個示例工程到workspace目錄下,被導(dǎo)出的工程與解壓縮后的SDK的layout稍有不同瘾英,它僅包含了該示例代碼所需的驅(qū)動和中間件枣接,并以一個平鋪的方式組織文件夾,但文件夾下的內(nèi)容并無區(qū)別缺谴。
NXP BLE SDK系統(tǒng)架構(gòu)
NXP提供了一套完整的BLE協(xié)議棧和編程框架,并已通過了Bluetooth Core Spec 5.0的認證耳鸯。用戶第一次開發(fā)自己的應(yīng)用時湿蛔,應(yīng)當先大致了解整個系統(tǒng)的組成,知道每個部分的職責(zé)县爬,分別有哪些文件阳啥。有了一些基礎(chǔ),再通過查詢API文檔财喳,很快便能掌握BLE應(yīng)用開發(fā)察迟。下圖為BLE SDK總體的框架圖,圖中使用了幾種不同的顏色來區(qū)分:
- 應(yīng)用(灰)
- 低功耗藍牙服務(wù)框架(黃)
- Connectivtiy Framework(綠)
- BLE協(xié)議棧(藍)
- 通用SDK(白)
OSA
OSA屬于Connectivity Framework耳高,單獨介紹它是因為OSA服務(wù)于整個SDK和協(xié)議棧扎瓶,并且也是系統(tǒng)復(fù)位后的入口。整個藍牙協(xié)議棧和SDK的設(shè)計使用了RTOS下才有的多任務(wù)泌枪,消息和事件等服務(wù)概荷。為了減少代碼大小和棧的內(nèi)存,適配一些資源緊張的SoC碌燕,在設(shè)計時才引入了OSA误证。
OSA提供一套接口封裝继薛,底層可以使用FreeRTOS或者Bare Metal前后臺兩種實現(xiàn)。如果底層為FreeRTOS愈捅,OSA只是簡單的將FreeRTOS API轉(zhuǎn)換為OSA API遏考。如果底層底層為Bare Metal系統(tǒng),OSA則內(nèi)部實現(xiàn)了一套多任務(wù)蓝谨、信號量灌具、事件等RTOS API的模擬實現(xiàn),當用戶調(diào)用OSA_Start啟動多任務(wù)調(diào)度環(huán)境時像棘,實際上是陷入了一個while(1)的循環(huán)稽亏,在這個循環(huán)中會不斷的檢查任務(wù)隊列的狀態(tài)并選擇優(yōu)先級最高的任務(wù)來運行,這些任務(wù)之間無法搶占缕题,因此每個任務(wù)的設(shè)計需要額外小心截歉,占用CPU的時間最好不要超過2ms,以確保處理BLE事件的任務(wù)能及時得到調(diào)度烟零。在編寫OSA的任務(wù)代碼時瘪松,有幾個不同與一般RTOS的地方需要注意。
- 任務(wù)體while(1)循環(huán)的最后需要加上一個條件判斷锨阿,如果是gUseRtos_c==0宵睦,則直接跳出循環(huán),相當于每次任務(wù)只執(zhí)行一次墅诡。
- 任務(wù)體while(1)循環(huán)之前的初始化工作需要特殊化處理壳嚎,定義一個靜態(tài)或者全局變量initailized的,并對它進行測試末早,確保在當OSA運行在Bare Metal配置時烟馅,這些初始化代碼僅會運行一次。
- OSA_EventWait, OSA_SemphoreWait, OSA_MutexLock等函數(shù)在OSA的FreeRTOS實現(xiàn)時會阻塞任務(wù)然磷,而在Bare Metal實現(xiàn)中郑趁,調(diào)用這些API不會阻塞,而是通過內(nèi)部一個狀態(tài)機來檢測資源姿搜,如果資源目前是不可獲得狀態(tài)寡润,則直接返回。
- OSA_TimeDelay函數(shù)在BM實現(xiàn)時是不會切換到其他任務(wù)運行的舅柜,而是在原地等待直到超時梭纹。
OSA的源碼可以在實現(xiàn)./middleware/wireless/framework_x.y.z/OSAbstraction下找到,如感興趣可以深入研讀业踢。
SDK提供的每一份BLE示例代碼中都提供了freertos和bm兩個工程栗柒。
Connectivity Framework
Connectivity Framework包含了眾多的子模塊,服務(wù)于應(yīng)用和BLE協(xié)議棧。本系列文章將會抽取其中幾個比較重要的幾個子模塊作為單獨的文章來分析瞬沦。本文希望先給讀者一個總體概念太伊,通過表格列出了各個模塊的職責(zé)和必要性。標注為“必要”的模塊如果缺失逛钻,最后整個BLE工程構(gòu)建將會失敗僚焦,說明該模塊已被協(xié)議棧或者其他的代碼引用了曙痘。標注為“非必要”的模塊目的是服務(wù)應(yīng)用層芳悲,如果用戶的應(yīng)用不需要這個功能,則可以移除边坤。
子模塊 | 職責(zé) | 必要性 | 說明 |
---|---|---|---|
FunctionLib | memcpy等標準函數(shù)的封裝 | 必要 | 通過頭文件配置選擇私有實現(xiàn)或直接使用C庫 |
List | 通用雙向鏈表實現(xiàn) | 必要 | List是MemManager和Messaging的基礎(chǔ) |
MemManager | 基于塊的內(nèi)存分配 | 必要 | 協(xié)議棧所需的動態(tài)內(nèi)存都從這里分配 |
Messaging | 消息隊列服務(wù) | 必要 | 不具備任務(wù)同步功能名扛,需配合OSA Event使用 |
OSAbstraction | 提供操作系統(tǒng)服務(wù)抽象 | 必要 | 整個系統(tǒng)調(diào)度都依賴于OSA |
TimersManager | 提供軟定時服務(wù) | 必要 | 協(xié)議棧超時機制需要使用到 |
SecLib | 封裝SMP加密操作 | 必要 | 部分芯片可以選擇使用硬件加密加速引擎 |
LowPower | 實現(xiàn)低功耗管理 | 非必要 | 僅當系統(tǒng)需要使用低功耗時用到 |
OtaSupport | 提供OTA服務(wù) | 非必要 | 僅包含OTA的應(yīng)用需要用到 |
SerialsManager | 提供不同串行通訊鏈路層的封裝 | 非必要 | 當配置協(xié)議棧為DTM模式或Host Only模式時需要) |
Flash | 對不同芯片內(nèi)部和外部Flash操作的封裝 | 非必要 | 當包括NVM模塊時為必要 |
NVM | 在Flash上提供一套非易失存儲機制 | 非必要 | 僅當系統(tǒng)需要用到Bonding時才為必要 |
MWS | 多協(xié)議共存協(xié)議 | 非必要 | 當需要與外部WLAN芯片配 |
Shell | 提供控制臺交互和格式化打印功能 | 非必要 | 服務(wù)于應(yīng)用程序 |
GPIO/LED/Keyboard | 提供簡單輸入輸出能力 | 非必要 | 服務(wù)于應(yīng)用程序 |
Reset/Panic/RNG | 對系統(tǒng)各個功能的簡單封裝 | 非必要 | 服務(wù)于應(yīng)用程序 |
BLE協(xié)議棧
BLE協(xié)議棧以閉源庫的形式提供,分為Host和Controller兩部分茧痒。下圖描述了組成Stack的主要文件和相互之間的依賴關(guān)系肮韧。Controller Stack負責(zé)與硬件和時序相關(guān)的藍牙鏈路層處理,它通過HCI接口與Host Stack進行協(xié)同工作完成BLE數(shù)據(jù)交互旺订。由于使用BLE SDK時幾乎不需要直接與Controller Stack打交道弄企,我們知道它的存在即可。
Host Stack是一個與硬件無關(guān)的C庫区拳,它通過HCI接口向Controller Stack發(fā)送命令和接收消息拘领,負責(zé)完成Bluetooth Core Spec中的L2CAP, ATT, SM, GATT, GAP等多個層次職責(zé)。Host Stack通過一組頭文件暴露出協(xié)議棧各個層的API和回調(diào)事件接口樱调,理解Host Stack提供的服務(wù)和數(shù)據(jù)結(jié)構(gòu)對于掌握BLE SDK開發(fā)至關(guān)重要约素。本系列專題將以獨立文章分享各個部分服務(wù)的具體應(yīng)用,這里先將協(xié)議棧的兩類主要接口介紹給大家笆凌。
API
API即應(yīng)用程序編程接口业汰,是外部應(yīng)用主動向協(xié)議棧發(fā)起請求的入口。協(xié)議棧所提供的絕大部分API都是異步的菩颖,利用Messaging消息隊列向協(xié)議棧的Host任務(wù)發(fā)送一條后便返回。實際的處理是在Host任務(wù)中完成为障,再通過某個注冊的callback回調(diào)函數(shù)來反饋執(zhí)行的結(jié)果晦闰。Callback注冊回調(diào)函數(shù)
Callback注冊回調(diào)函數(shù)是由應(yīng)用程序提供的一個鉤子函數(shù),在協(xié)議棧內(nèi)部處理中如果有需要通知到用戶層事件發(fā)生或者數(shù)據(jù)變化時鳍怨,將調(diào)用注冊的鉤子函數(shù)通知應(yīng)用程序做出響應(yīng)動作呻右。
下面一段示例代碼演示了Gap_Connect()和Gap_Disconnect()兩個異步API的行為和他們所觸發(fā)的callback回調(diào)函數(shù)后對應(yīng)事件的響應(yīng)。通過這段代碼可以大致了解應(yīng)用代碼如何與協(xié)議棧進行交互鞋喇。
// 1. initiate API to request and connection and register a callback
bleResult_t rt;
rt = Gap_Connect(&gConnReqParams, Gap_ConnectionCallback);
if (rt != gBleSuccess_c) {
printf("Gap_Connect API fail, reason = %d\r\n", rt);
}
// when PC(R15) arrives here, the connection may NOT be setup
...
// 2. initiate API to request a disconnection action.
rt = Gap_Disconnect(peerDeviceId);
if (rt != gBleSuccess_c) {
printf("Gap_Disconnect fail, reason = %d\r\n", rt);
}
// when PC(R15) arrives here, the connection may NOT be disconnected
...
// 3. Event will be triggered when connection and disconnection happens
void Gap_ConnectionCallback(deviceId_t peerDeviceId, gapConnectionEvent_t* pConnectionEvent)
{
switch (pConnectionEvent->eventType)
{
case gConnEvtConnected_c:
// async event to notify that link is connected
printf("Connected to device (%d)\r\n", peerDeviceId);
break;
case gConnEvtDisconnected_c:
// async event to notify that link is disconnected
printf("Disconnected to device (%d)\r\n", peerDeviceId);
break;
...
低功耗藍牙服務(wù)框架
低功耗藍牙服務(wù)框架這個名稱是筆者取給SDK中非協(xié)議棧部分的藍牙代碼取一個名字声滥,涉及內(nèi)容主要包括有HCI、FSCI、Profiles落塑、Connection Manager, Service Discovery, App Thread, BLE Initialization, Stack Runtime Environment等幾個部分纽疟,顧名思義是這些代碼都是為實現(xiàn)BLE應(yīng)用而服務(wù)的,目的是簡化用戶開發(fā)的難度憾赁。
- Stack Runtime Environment
上一小節(jié)介紹的Host和Controller協(xié)議棧庫提供的是一組可鏈接的符號污朽,需要外部創(chuàng)建任務(wù)以提供運行時環(huán)境和創(chuàng)建必要的隊列和事件資源,這就是Stack Runtime Environment的職責(zé)龙考。協(xié)議棧提的了兩個任務(wù)處理函數(shù)Host_TaskHandler()和Controller_TaskHandler()蟆肆,分別在創(chuàng)建的Host_task和Controller_task里調(diào)用。另外任務(wù)處理函數(shù)還需要用到隊列和事件資源晦款,用戶任務(wù)與Host_task交互炎功、以及Host_task與Controller_task的交互都依賴于他們。下表說明了所創(chuàng)建資源的用途缓溅。
資源 | 類別 | 用途 |
---|---|---|
gApp2Host_TaskQueue | 消息隊列 | 用戶任務(wù)調(diào)用Host Stack API時蛇损,向這個隊列插入一個請求 |
gHci2Host_TaskQueue | 消息隊列 | Controller Taskdan產(chǎn)生HCI事件時,向這個隊列插入一個請求 |
gHost_TaskEvent | OSA事件 | 用于通知Host Stack隊列中有新的消息插入 |
mControllerTaskEvent | OSA事件 | Host Task向Controller Task發(fā)送HCI命令時或中斷向Controller Task請求服務(wù) |
- App Thread
App Thread是在系統(tǒng)啟動后由OSA創(chuàng)建的第一個任務(wù)肛宋,它在完成BLE系統(tǒng)初始化后創(chuàng)建了一個應(yīng)用后臺州藕,主要目的是服務(wù)Host stack觸發(fā)的各類callback回調(diào)事件。通過Host_stack提供的Registeration API注冊的回調(diào)函數(shù)被調(diào)用的上下文都是前面介紹的Host task里酝陈,如果在這些callback做一些耗時較長的用戶動作(比如上面的printf打印輸出)床玻,Host task其他部分會得不到響應(yīng),從而影響B(tài)LE協(xié)議交互沉帮。因此很有必要將這些callback回調(diào)事件的處理放到一個相對低優(yōu)先級任務(wù)中锈死,這個任務(wù)就是App Thread。首先Applmain.c中對Host stack提供的回調(diào)函數(shù)接口都做了實現(xiàn)穆壕,這些實現(xiàn)并不作實際處理待牵,而是轉(zhuǎn)發(fā)一個消息到App Thread。App Thread一直處于等待事件的狀態(tài)喇勋,收到事件后再分發(fā)到各個自定義的callback中作相應(yīng)處理缨该。App Thread同時還可以通過等待另外一個隊列來接收其他任務(wù)發(fā)送的Application消息并處理,以方便用戶實現(xiàn)類似于協(xié)議棧的異步處理機制川背。下表列出了App Thread的隊列和時間資源以及他們的用途贰拿。
資源 | 類別 | 用途 |
---|---|---|
mHostAppInputQueue | 消息隊列 | Host Stack產(chǎn)生的callback里將向這個隊列插入一個請求 |
mAppCbInputQueue | 消息隊列 | App Thread本身或者其他用戶任務(wù)都可以向這個隊列插入一個請求 |
mAppEvent | OSA事件 | 用于通知App Thread有隊列里有新的消息插入 |
下面代碼段以App_Connect為例來展示了代碼是如何通過App Thread來處理Host stack回調(diào)函數(shù)的。
// User call App_Connect, instead of Gap_Connect
bleResult_t App_Connect(gapConnectionRequestParameters_t* pParameters,
gapConnectionCallback_t connCallback)
{
pfConnCallback = connCallback;
return Gap_Connect(pParameters, App_ConnectionCallback);
}
// Simplified App_ConnectionCallback, the function insert a message and notify App Thread
void App_ConnectionCallback (deviceId_t peerDeviceId, gapConnectionEvent_t* pConnectionEvent)
{
appMsgFromHost_t *pMsgIn = NULL;
uint8_t msgLen = GetRelAddr(appMsgFromHost_t, msgData) + sizeof(connectionMsg_t);
pMsgIn = MSG_Alloc(msgLen);
// use Applmain defined message type
pMsgIn->msgType = gAppGapConnectionMsg_c;
pMsgIn->msgData.connMsg.deviceId = peerDeviceId;
FLib_MemCpy(&pMsgIn->msgData.connMsg.connEvent, pConnectionEvent, sizeof(gapConnectionEvent_t));
MSG_Queue(&mHostAppInputQueue, pMsgIn);
OSA_EventSet(mAppEvent, gAppEvtMsgFromHostStack_c);
}
// Pseduo code of App_Thread
void App_Thread (uint32_t param)
{
while(1) {
OSA_EventWait(mAppEvent, osaEventFlagsAll_c, FALSE, osaWaitForever_c , &event);
if (event & gAppEvtMsgFromHostStack_c) {
while (MSG_Pending(&mHostAppInputQueue)) {
pMsgIn = MSG_DeQueue(&mHostAppInputQueue);
// check msgType and dispatch callback handler
if (pMsg->msgType == gAppGapConnectionMsg_c) {
pfConnCallback(pMsg->msgData.connMsg.deviceId, &pMsg->msgData.connMsg.connEvent);
} else if (pMsg->msgType == ...) {
通過下圖可以全面的了解到App Thread, Host task和Controller task三者之間是如何完成交互的熄云。掌握了這張圖的消息交互流程膨更,開發(fā)基于NXP BLE SDK的應(yīng)用也就變的非常容易了。
HCI
HCI是低功耗藍牙Controller協(xié)議棧和Host協(xié)議棧之間的溝通橋梁缴允,對于SoC荚守,兩個協(xié)議棧都運行在同一芯片上,這部分功能實際上是簡單的函數(shù)調(diào)用。當BLE SDK被配置為只作為controller Only模式(用于DTM)矗漾,或者Host Only模式(controller使用另外一顆芯片)時锈候,HCI的就需要與通過SerialManager來完成與外界通訊。FSCI
FSCI是Framework Serial Communication Interface的縮寫缩功,該模塊定義了一套統(tǒng)一的接口將無線通訊微控制器(BLE晴及,Thread,Zigbee)所提供的服務(wù)提供給另外一顆主處理器或者PC系統(tǒng)嫡锌。對于BLE SoC而言虑稼,片上運行了完整的BLE Host和Controller協(xié)議棧,此時處于Network Processor網(wǎng)絡(luò)處理器模式势木。外部處理器在只需要遵循FSCI協(xié)議便能控制SoC發(fā)起廣播蛛倦、建立連接和進行數(shù)據(jù)交換。Profiles
在GATT之上啦桌,Bluetooth SIG定義了多個標準的GATT based Profile幫助應(yīng)用層互聯(lián)互通溯壶,同時每個廠家也會定義一些簡單的私有Profile來實現(xiàn)raw數(shù)據(jù)傳輸、OTA等功能甫男。由于所有的Callback回調(diào)事件處理都是在應(yīng)用中完成的且改,Profile在NXP BLE SDK中職責(zé)比較簡單,主要負責(zé)完成Profile數(shù)據(jù)到GATT數(shù)據(jù)庫的操作轉(zhuǎn)換板驳。Conneciton Manager 與 Service Discovery
連接建立和服務(wù)發(fā)現(xiàn)是每個BLE應(yīng)用都需要經(jīng)歷的過程又跛,BLE SDK將這部分通用的代碼從應(yīng)用層分離了出來,形成兩對獨立的.c/.h文件ble_conn_manager.c/h和ble_service_discovery.c/h若治。這樣減少了應(yīng)用層重復(fù)的代碼慨蓝,提高代碼可維護性。這兩個部分服務(wù)的功能在后面介紹藍牙SDK具體編程的文章中都會有所涉及端幼。GATT Database
通過一套宏定義的方法礼烈,完成靜態(tài)或者動態(tài)GATT數(shù)據(jù)庫的建立。第一次看到gatt_db.h會很難以理解里面的特殊格式婆跑,但要是“照貓畫虎”的增加自己的service或者characeteristic還是比較簡單的此熬,這就是它的神奇之處。寫一篇文章專門講解gatt_db模塊到底是如何通過一系列的宏定義來成這個工作的滑进。
應(yīng)用
BLE SDK的每個應(yīng)用示例都包含了十分類似的應(yīng)用層文件摹迷,下表簡單描述了各個文件的職責(zé)。其中<user_app>.c/h是整個應(yīng)用的核心郊供。
文件名 | 職責(zé) |
---|---|
app_config.c | BLE廣播參數(shù)、掃描參數(shù)近哟、安全性相關(guān)的配置 |
app_preinclude.h | 全局參數(shù)配置文件(其他文件不需要include此文件驮审,在IDE里已配置) |
gatt_db.h | GATT database描述文件 |
gatt_uuid128.h | 私有128位UUID描述文件 |
<user_app>.c/h | 應(yīng)用示例代碼,如heart_rate_sensor.c,廣播發(fā)起疯淫、掃描地来,以及各種BLE協(xié)議棧事件的處理 |
小結(jié)
本文從零開始介紹NXP BLE SDK系統(tǒng)架構(gòu),逐步分析了各個子模塊的功能熙掺。對于BLE協(xié)議棧和服務(wù)框兩部分作了較為細致的講解未斑,以幫助大家了解自己編寫的應(yīng)用是如何與框架進行交互的。本文是《深入NXP低功耗藍牙SDK開發(fā)系列》的第一篇币绩,涵蓋內(nèi)容較廣蜡秽。如果您覺得其中某部分功能的介紹太過簡單,不要心急缆镣,留言給我芽突,后續(xù)會有多篇文章一步步教大家如何上手BLE SDK開發(fā),敬請期待吧:)董瞻。