本系列教程將結合TI推出的CC254x SoC 系列,講解從環境的搭建到藍牙4.0協議棧的開發來深入學習藍牙4.0的開發過程。教程共分為六部分,本文為第三部分:
第三部分知識點:
第十一節 串口通信
第十二節 Flash的讀寫
第十三節 BLE協議棧簡介
第十四節 OSAL工作原理
第十五節 BLE藍牙4.0協議棧啟動分析
?
有關TI 的CC254x芯片介紹,可點擊下面鏈接查看:
?
有關本文的工具下載,大家可以到以下這個地址:
朱兆祺ForARM?
第十一節 串口通信
在軟件開發過程中調試是一個很關鍵的過程,而調試用的最多的手段就是打印Log,嵌入式平臺很少有顯示設備,所以我們需要將信息通過串口打印到PC端。
MT254xboard上已經通過RS232芯片將UART0連接到DB9,我們只需要將DB9連接到電腦即可,UART0 對應的外部設備 IO 引腳關系為:P0_2------RX,P0_3------TX。
我們需要將這兩個IO配置為復用功能,CC2540的USART可以配置為SPI模式或者異步UART模式,這里我們需要配置為異步UART模式。
首先配置IO為UART模式:
PERCFG &= ~0x01; // 配置UART為位置 1
P0SEL = 0x3c; // P0_2,P0_3,P0_4,P0_5用作串口功能
P2DIR &= ~0XC0; // P0 優先作為UART0
配置UART0寄存器,將UART0配置為8N1模式,波特率為115200。
U0CSR |= 0x80; // UART 方式
U0GCR |= 11; // U0GCR與U0BAUD配合
U0BAUD |= 216; // 波特率設為115200
UTX0IF = 0; // 清除中斷標志
U0CSR |= 0X40; // 允許接收
IEN0 |= 0x84; // 開總中斷,接收中斷
這里采用中斷方式來接收串口數據,并在中斷中回調應用層的接收處理函數。
#pragma vector = URX0_VECTOR
__interrupt void UART0_ISR(void)
{
uint8 ch;
URX0IF = 0; // 清中斷標志
ch = U0DBUF;
if ( NULL != RecvCb ) // 調用回調函數
{
RecvCb(ch);
}
}
為了測試串口的通訊功能,這里我們通過串口接收命令的方式來控制LED的亮滅和蜂鳴器的響和停止,并且顯示當前的狀態。根據串口輸出提示,發送對應字符可以實現相應功能,并且顯示狀態。
第十二節 Flash的讀寫
嵌入式系統中需要存儲數據,而片內的Flash資源很匱乏,所以我們經常需要使用SpiFlash來存儲數據,MT254xboard中板載了一個 512Kbyte的Flash,下面我們來驅動此Flash。上一小節中我們用SPI的方式驅動了LCD12864,這節我們繼續用SPI來驅動板載的 Flash,
下面我們來檢測這個Flash,檢測的方法為,全部寫入0xAA,然后再讀出,對比是否為0xAA,如果是,那Flash是沒有問題的,否則Flash可能已經有壞塊。具體的代碼見例程,這個過程所需要的時間取決于我們需要檢測的區域大小,如果完全檢測,則可能需要幾分鐘的時間。
int main(void)
{
SysStartXOSC();
LCD12864_Init(); // LCD初始化
GD25Q40_Init(); // Flash初始化
LCD12864_DisStr(0, “Flash Check.。。。”);
sprintf(LCDBuf, “Flash ID :%04X”, GD25Q40_ReadID()); // 讀取器件ID
LCD12864_DisStr(1, LCDBuf);
GD25Q40_EraseChip(); // 擦除整片Flash 大約需要10S
LCD12864_DisStr(2, “Erase Chip Complete”);
uint32 iCnt = 0;
// 全部寫入0xAA
const uint8 Write = 0xAA;
for(iCnt=0; iCnt < CHECK_ADDR_RANGE; iCnt++)
{
GD25Q40_Write(&Write, iCnt, 1); // 寫入0xAA
}
// 讀取Flash內部的值,與寫入的值對比
uint8 Read;
for(iCnt=0; iCnt < CHECK_ADDR_RANGE; iCnt++)
{
GD25Q40_Read(&Read, iCnt, 1);
if(Read != Write)
{
LCD12864_DisStr(3, “Flash Error”);
break;
}
}
// 寫入的值與讀出的值完全一樣
if(iCnt >= CHECK_ADDR_RANGE)
{
LCD12864_DisStr(3, “Flash Check Success”);
}
GD25Q40_EraseChip(); // 再次擦除
while(1);
return 0;
}
MT254X藍牙4.0開發板Flash效果:
第十三節 BLE協議棧簡介
TI的協議棧分為兩部分:控制器和主機。對于4.0以前的藍牙,這兩部分是分開的。所有profile和應用都建構在GAP或GATT之上。根據這張圖,我們從底層開始介紹。TI的這款CC2540器件可以單芯片實現BLE藍牙協議棧結構圖的所有組件,包括應用程序。
1.1.1 PHY層
1Mbps自適應跳頻GFSK(高斯頻移鍵控),運行在免證的2.4GHz。
1.1.2 LL層
LL層為RF控制器,控制設備處于準備(standby)、廣播、監聽/掃描(scan)、初始化、連接,這五種狀態中一種。五種狀態切換描述為:未連接時,設備廣播信息,另外一個設備一直監聽或按需掃描,兩個設備連接初始化,設備連接上了。發起聊天的設備為主設備,接受聊天的設備為從設備,同一次聊天只能有一個意見領袖,即主設備和從設備不能切換。
1.1.3 HCI層
HCI層為接口層,向上為主機提供軟件應用程序接口(API),對外為外部硬件控制接口,可以通過串口、SPI、USB來實現設備控制。
1.1.4 L2CAP層
L2CAP層提供數據封裝服務,允許邏輯上的點對點通訊。
1.1.5 SM層
SM層提供配對和密匙分發,實現安全連接和數據交換。
1.1.6 ATT層
ATT層負責數據檢索,允許設備向另外一個設備展示一塊特定的數據稱之為屬性,在ATT環境中,展示屬性的設備稱之為服務器,與它配對的設備稱之為客戶端。鏈路層的主機從機和這里的服務器、客服端是兩種概念,主設備既可以是服務器,也可以是客戶端。從設備毅然。
1.1.7 GATT層
GATT層定義了使用 ATT 的服務框架和配置文件(profiles)的結構。BLE 中所有的數據通信都需要經過 GATT。GATT負責處理向上與應用打交道,其關鍵工作是把為檢索工作提供合適的profile結構,而profile由檢索關鍵詞(characteristics)組成。
1.1.8 GAP層
GAP直接與應用程序或配置文件(profiles)通信的接口,處理設備發現和連接相關服務。另外還處理安全特性的初始化。對上級,提供應用程序接口,對下級,管理各級職能部門,尤其是指示LL層控制室五種狀態切換,指導保衛處做好機要工作。
1.2 TI協議棧源碼介紹
在第二章我們講解了源碼的安裝,這里我們就來剖析源碼的結構。打開協議棧目錄我們可以看到下圖:
BLE源碼:
目錄名
內容說明
Accessories一些工具和已經編譯好的Hex文件此文件夾中有Btool的安裝包、USB-CDC的驅動。
ComponentsHal驅動,OSAL源碼、協議棧通用源碼此文件夾是OSAL各層組件的實現
Documents幫助文檔協議棧說明文檔,這是學習BLE最好的資料。
Projects工程文件這里有一些TI的Demo,我們開發一般是在Demo的基礎上進行
這里TI給出了很多Demo,這些例程都是經過了SIG評審的,ble 文件夾中有很多工程文件,有些是具體的應用,例如BloodPressure、GlucoseCollector 、GlucoseSensor 、 HeartRate 、HIDEmuKbd 等都為傳感器的實際應用,有相應標準的 Profile。
其中有4種角色: SimpleBLEBroadcaster 、 SimpleBLECentral 、SimpleBLEObserver、SimpleBLEPeripheral。
他們都有自己的特點。
1.Broadcaster 廣播員 —— 非連接性的信號裝置
2.Observer 觀察者 —— 掃描得到,但不能鏈接
3.Peripheral 從機 —— 可鏈接,在單個鏈路層鏈接中作為從機
4.Central 主機 —— 掃描設備并發起鏈接,在單鏈路層或多鏈路層中作為主機。
我們的講解將圍繞這主機和從機進行。因為其它的設備都是基于這兩種設備擴展開來的。
第十四節 OSAL工作原理
藍牙為了實現同多個設備相連,或實現多功能,也實現了功能擴充,這就產生了調度問題。因為,雖然軟件和協議棧可擴充,但終究最底層的執行部門只有一個。為了實現多事件和多任務切換,需要把事件和任務對應的應用,并起一個名字OSAL操作系統抽象層。
OSAL管理的實現
如果實現軟件和硬件的低耦合,使軟件不經改動或很少改動即可應用在另外的硬件上,這樣就方便硬件改造、升級、遷移后,軟件的移植。HAL硬件抽象層正是用來抽象各種硬件的資源,告知給軟件。其作用類似于嵌入式系統設備驅動的定義硬件資源的h頭文件。
BLE低功耗藍牙系統架構:
OSAL作為調度核心,BLE協議棧、profile定義、所有的應用都圍繞它來實現。OSAL不是傳統大家使用的操作系統,而是一個允許軟件建立和執行事件的循環。
軟件功能是由任務事件來實現的,創建一個任務事件需要以下工作:
1. 創建task identifier任務ID;
2. 編寫任務初始化(task initialization routine)進程,并需要添加到OSAL初始化進程中,這就是說系統啟動后不能動態添加功能;
3. 編寫任務處理程序;
4. 如有需要提供消息服務。
BLE協議棧的各層都是以OSAL任務方式實現,由于LL控制室的時間要求最為迫切,所以其任務優先級最高。為了實現任務管理,OSAL通過消息處理(messageprocess),存儲管理,計時器定時等附加服務實現。
系統啟動流程:
為了使用OSAL,在main函數的最后要啟動一個名叫osal_start_system的進程,該進程會調用由特定應用決定的啟動函數 osalInitTasks(來啟動系統)。osalInitTasks逐個調用BLE協議棧各層的啟動進程來初始化協議棧。隨后,設置一個任務的 8bit任務ID(task ID),跳入循環等待執行任務,系統啟動完成。
1. 任務優先級決定于任務ID,任務ID越小,優先級越高
2. BLE協議棧各層的任務優先級比應用程序的高
3. 初始化協議棧后,越早調入的任務,任務ID越高,優先級越低,即系統傾向于處理新到的任務
每個事件任務由對應的16bit事件變量來標示,事件狀態由旗號(taskflag)來標示。如果事件處理程序已經完成,但其旗號并沒有移除,OSAL會認為事情還沒有完成而繼續在該程序中不返回。比如,在SimpleBLEPeripheral實例工程中,當事件START_DEVICE_EVT發生,其處理函數SimpleBLEPeripheral_ProcessEvent就運行,結束后返回16bit事件變量,并清除旗語 SBP_START_DEVICE_EVT。
每當OSAL事件檢測到了有任務事件,其相應的處理進程將被添加到由處理進程指針構成的事件處理表單中,該表單名叫taskArr(taskarray)。taskArr中各個事件進程的順序和osalInitTasks初始化函數中任務ID的順序是對應的。
有兩種,最簡單的方法是使用osal_set_event函數(函數原型在OSAL.h文件中),在這個函數中,用戶可以像定義函數參數一樣設置任務ID 和事件旗語。第二種方法是使用osal_start_timerEx函數(函數原型在OSAL_Timers.h文件中),使用方法同 osal_set_event函數,而第三個以毫秒為單位的參數osal_start_timerEx則指示該事件處理必須要在這個限定時間內,通過定時器來為事件處理計時。
類似于Linux嵌入式系統內存分配C函數mem_alloc,OSAL利用osal_mem_alloc提供基本的存儲管理,但osal_mem_alloc只有一個用于定義byte數的參數。對應的內存釋放函數為osal_mem_free。
不同的子系統通過OSAL的消息機制通信。消息即為數據,數據種類和長度都不限定。消息收發過程描述如下:
接收信息,調用函數osal_msg_allocate創建消息占用內存空間(已經包含了osal_mem_alloc函數功能),需要為該函數指定空間大小,該函數返回內存空間地址指針,利用該指針就可把所需數據拷貝到該空間。
發送數據,調用函數osal_msg_send,需為該函數指定發送目標任務,OSAL通過旗語SYS_EVENT_MSG告知目標任務,目標任務的處理函數調用osal_msg_receive來接收發來的數據。建議每個OSAL任務都有一個消息處理函數,每當任務收到一個消息后,通過消息的種類來確定需要本任務做相應處理。消息接收并處理完成,調用函數osal_msg_deallocate來釋放內存(已經包含了osal_mem_free函數功能)。
為了實現更好的移植性,協議棧將硬件層抽象出了一個HAL硬件抽象層,當新的硬件平臺做好后,只需修改HAL,而不需修改HAL之上的協議棧的其他組件和應用程序。
第十五節 BLE藍牙4.0協議棧啟動分析
TI的這款CC2540/CC2541器件可以單芯片實現BLE藍牙協議棧結構圖的所有組件,包括應用程序。從這章開始我們來剖析協議棧源碼,我們選用 SimpleBLEPeripheral工程開刀,這是一個從機的例程,基本的工作是對外廣播,等待主機來連接,讀寫展示的屬性。
首先打開工程文件,打開后可以看到整個工程的結構。
我們按照系統的啟動順序來一步一步走,我們都知道在C代碼中,一般啟動的首個函數為main,這個函數在 SimpleBLEPeripheral_Main.c中,打開文件,可以看到這個文件只有一個main函數和一個函數的申明,我們暫時不理會那個申明的函數,先看main都做了些什么工作:
Int main(void)
{
/* Initialize hardware */
HAL_BOARD_INIT(); // 硬件初始化
// Initialize board I/O
InitBoard( OB_COLD ); // 板級初始化
/* Initialze the HAL driver */
HalDriverInit(); // Hal驅動初始化
/* Initialize NV system */
osal_snv_init(); // Flash存儲SNV初始化
/* Initialize LL */
/* Initialize the operating system */
osal_init_system(); // OSAL初始化
/* Enable interrupts */
HAL_ENABLE_INTERRUPTS(); // 使能總中斷
// Final board initialization
InitBoard( OB_READY ); // 板級初始化
#if defined ( POWER_SAVING )
osal_pwrmgr_device( PWRMGR_BATTERY ); // 低功耗管理
#endif
/* Start OSAL */
osal_start_system(); // No Return from here 啟動OSAL
return 0;
}
通過代碼我們可以看到,系統啟動的過程,主要是做了一些初始化,如果開啟了低功耗,則還需要開啟低功耗管理。我們先不去理會初始化做了什么,但是我們知道在main函數的最后啟動了OSAL,那么我們就進去看看OSAL是如何運作的。
在IAR中如果需要跳轉到某個函數或變量的定義,可以在此函數名中右擊然后選擇Go To Definition……就可以調到相應的定義。
void osal_start_system( void )
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;) // Forever Loop
#endif
{
osal_run_system();
}
}
這里看到我們進入了一個死循環,并且一直調用osal_run_system(),那我們再進入此函數。
void osal_run_system( void ){
uint8 idx = 0;
#ifndef HAL_BOARD_CC2538
osalTimeUpdate(); // 定時器更新
#endif
Hal_ProcessPoll(); // Hal層信息處理
do {
if (tasksEvents[idx]) // Task is highest priority that is ready.
{
break;
}
} while (++idx < tasksCnt); // 檢查每個人任務是否有事件
if (idx < tasksCnt) // 有事件發生
{
uint16 events;
halIntState_t intState;
HAL_ENTER_CRITICAL_SECTION(intState); // 進入臨界區
events = tasksEvents[idx];
tasksEvents[idx] = 0; // Clear the Events for this task. 清除事件標志
HAL_EXIT_CRITICAL_SECTION(intState); // 退出臨界區
activeTaskID = idx;
events = (tasksArr[idx])( idx, events ); // 執行事件處理函數
activeTaskID = TASK_NO_TASK;
HAL_ENTER_CRITICAL_SECTION(intState); // 進入臨界區
tasksEvents[idx] |= events; // Add back unprocessed events to the current task.
HAL_EXIT_CRITICAL_SECTION(intState); // 退出臨界區
}
#if defined( POWER_SAVING ) // 沒有事件發生,并且開啟了低功耗模式
else // Complete pass through all task events with no activity?
{ // 系統進入低功耗模式
osal_pwrmgr_powerconserve(); // Put the processor/system into sleep
}
#endif
/* Yield in case cooperative scheduling is being used. */
#if defined (configUSE_PREEMPTION) && (configUSE_PREEMPTION == 0)
{
osal_task_yield();
}
#endif
}
在這里可以看到這個OSAL的核心,整個OSAL通過檢測每個任務是否有事件發生,如果有則執行相應的任務,處理相應的事件。如果沒有事件需要處理并且開啟了低功耗模式,則系統就會進入低功耗模式。
這里有一個很關鍵的地方,OSAL是如何知道哪個事件需要哪個任務來處理呢?
events = (tasksArr[idx])( idx, events ); // 執行事件處理函數
我們看這里有一個很關鍵的數組tasksArr,很顯然,這是一個函數指針數組,我們看看它的定義。
const pTaskEventHandlerFn tasksArr[] =
{
LL_ProcessEvent, // task 0
Hal_ProcessEvent, // task 1
HCI_ProcessEvent, // task 2
#if defined ( OSAL_CBTIMER_NUM_TASKS )
OSAL_CBTIMER_PROCESS_EVENT( osal_CbTimerProcessEvent ), // task 3
#endif
L2CAP_ProcessEvent, // task 4
GAP_ProcessEvent, // task 5
GATT_ProcessEvent, // task 6
SM_ProcessEvent, // task 7
GAPRole_ProcessEvent, // task 8
GAPBondMgr_ProcessEvent, // task 9
GATTServApp_ProcessEvent, // task 10
SimpleBLEPeripheral_ProcessEvent // task 11
};
可以看到在這個數組的定義中,每個成員都是任務的執行函數,按照任務的優先級排序,并且在osalInitTasks中初始化的時候,我們可以看到每個任務都有一個對應的初始化函數,并且傳遞了一個taskID,此ID從0開始自增,這里有一點非常重要,初始化的順序和任務數組的定義順序是一樣的,這就保證了我們給任務發生消息或事件時能夠準確的傳遞到相應的任務處理函數。
void osalInitTasks( void )
{
uint8 taskID = 0;
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
/* LL Task */
LL_Init( taskID++ );
/* Hal Task */
Hal_Init( taskID++ );
/* HCI Task */
HCI_Init( taskID++ );
#if defined ( OSAL_CBTIMER_NUM_TASKS )
/* Callback Timer Tasks */
osal_CbTimerInit( taskID );
taskID += OSAL_CBTIMER_NUM_TASKS;
#endif
/* L2CAP Task */
L2CAP_Init( taskID++ );
/* GAP Task */
GAP_Init( taskID++ );
/* GATT Task */
GATT_Init( taskID++ );
/* SM Task */
SM_Init( taskID++ );
/* Profiles */
GAPRole_Init( taskID++ );
GAPBondMgr_Init( taskID++ );
GATTServApp_Init( taskID++ );
/* Application */
SimpleBLEPeripheral_Init( taskID );
}
應用層的初始化SimpleBLEPeripheral_Init,SimpleBLEPeripheral_Init( uint8task_id )主要對 GAP 和 GATT 進行配置,最后調用osal_set_event(simpleBLEPeripheral_TaskID, SBP_START_DEVICE_EVT )啟動設備。
設備啟動后應用層就能接收到這個設置的事件并進行處理,可以看到設備啟動中主要是啟動設備,注冊綁定管理,并且啟動了一個定時器,這個定時器是一個周期事件的第一次啟動。
周期事件中每次都會重啟這個定時器,并且處理周期事件。
在初始化的時候我們注冊了一個很重要的函數,設備狀態改變時的回調函數,這個函數在設備的狀態改變時會被底層的協議棧回調,我們可以從這個回調函數中看的設備的狀態的改變。
static void peripheralStateNotificationCB( gaprole_States_t newState);
從函數的定義可以看出,設備的狀態類型都在數據類型gaprole_States_t中定義了,我們看一下這個數據類型的定義:
typedef enum
{
GAPROLE_INIT = 0, //!< Waiting to be started
GAPROLE_STARTED, //!< Started but not advertising
GAPROLE_ADVERTISING, //!< Currently Advertising
GAPROLE_WAITING, //!< Device is started but not advertising, is in waiting period before advertising again
GAPROLE_WAITING_AFTER_TIMEOUT, //!< Device just timed out from a connection but is not yet advertising, is in waiting period before advertising again
GAPROLE_CONNECTED, //!< In a connection
GAPROLE_CONNECTED_ADV, //!< In a connection + advertising
GAPROLE_ERROR //!< Error occurred - invalid state
} gaprole_States_t;
看到這個定義就很明確了,設備的狀態就在這幾種狀態間切換。
本文導航
- 第 1 頁:由淺入深,藍牙4.0/BLE協議棧開發攻略大全(3)
- 第 2 頁:第十二節 Flash的讀寫
- 第 3 頁:第十三節 BLE協議棧簡介
- 第 4 頁:第十四節 OSAL工作原理
- 第 5 頁:第十五節 BLE藍牙4.0協議棧啟動分析
- TI公司(73141)
- 協議棧(33351)
- 藍牙BLE(23960)
評論