原創博客,如有轉載,註明出處——在金華的電子民工林。
當初本人寫在另外一個論壇上,現在移到這邊來。希望幫到更多人。
本文采用的是CC2640R2F1.40協議棧版本。高階版本可做參考。
做完一個central程序,就記一下流水,大致寫下自己從一個工程如何從0開始做。
每個人接到一個項目任務,相信不僅僅是程序上的編寫,還有整個工程的管理,這些做的好,以後自己修改方便,移植也方便,所以這次記下自己的流水賬,用來拋磚引玉,供大家參考。
首先,我用的是CCS,說實話,寫CC2640R2F強烈推薦使用CCS,非常方便,好用,還便於管理,移植等等。
由於CCS,對於每個工程,使用的是workspace,其實就是一個文件夾,攜帶了一個工程的各種配置和程序而已。對於我們使用者來說,一個workspace就是一個工程了。我的個人習慣是把所有CC2640R2F的workspace集中在一個工作盤符(在公司裏是D盤)的一級子目錄下,這樣更便於管理,注意的一點是,所有workspace的路徑,全部使用英文,使用中文字符容易出問題。如下圖:
所以,開啓一個新的工程,第一件事,就是在這個Group目錄下,新建一個文件夾,然後修改成工程的名字,打開CCS,有個switch workspace選項,將workspace指向這個文件夾,一個新的工程開始!
注:以後所有切換工程,都是通過這個切換。
注:有過工程導入經驗的,及看過香瓜寫的《簡單粗暴學藍牙》的跳過以下分割線部分
建好workspace以後,就是導入工程了,**CCS的一個優點,就是原廠的協議棧,和我們寫的工程是分離的,我們導入工程,只是對工程的一個複製,從安裝文件夾複製到我們的workspace裏而已,所以就不怕我們不小心修改到哪裏了,導致沒有原廠協議棧參考。**我這次是寫一個central工程,那麼我就導入central的工程就行了,我是默認安裝協議棧在C盤的,所以協議棧在C盤的ti文件夾下,各個demo路徑如下圖:
好了,導入以後,基本工作就差最後一步了,就是先編譯stack_library,再編譯app。記住這個順序。編譯通過了,那正式開始搞程序了!
…………………………………………………………我是分割線………………………………………………………………………………
學CC2640R2F的第一個入門是軟件的安裝與編譯,第二個入門就是程序從哪裏開始讀。只要搞清楚這兩點,後邊的就是C語言程序閱讀的事了,對於老鳥菜鳥,都是本職工作,相信只要時間花下去,都能有收穫。
那麼現在來說說程序從哪開始讀,對於新手很重要。大家都知道,所有的程序的都是從main開始,新手最頭痛的就是main在哪,現在指給大家:
非常好找,就在app的startup文件夾裏的這個main文件,我們點開,看看都是什麼:
int main()
{
#if defined( USE_FPGA )
HWREG(PRCM_BASE + PRCM_O_PDCTL0) &= ~PRCM_PDCTL0_RFC_ON;
HWREG(PRCM_BASE + PRCM_O_PDCTL1) &= ~PRCM_PDCTL1_RFC_ON;
#endif // USE_FPGA
/* Register Application callback to trap asserts raised in the Stack */
RegisterAssertCback(AssertHandler);
// PIN_init(BoardGpioInitTable);
PIN_init(UserGpioInitTable);
#if defined( USE_FPGA )
// set RFC mode to support BLE
// Note: This must be done before the RF Core is released from reset!
SET_RFC_BLE_MODE(RFC_MODE_BLE);
#endif // USE_FPGA
// Enable iCache prefetching
VIMSConfigure(VIMS_BASE, TRUE, TRUE);
// Enable cache
VIMSModeSet(VIMS_BASE, VIMS_MODE_ENABLED);
#if !defined( POWER_SAVING ) || defined( USE_FPGA )
/* Set constraints for Standby, powerdown and idle mode */
// PowerCC26XX_SB_DISALLOW may be redundant
Power_setConstraint(PowerCC26XX_SB_DISALLOW);
Power_setConstraint(PowerCC26XX_IDLE_PD_DISALLOW);
#endif // POWER_SAVING | USE_FPGA
#ifdef ICALL_JT
user0Cfg.appServiceInfo->timerTickPeriod = Clock_tickPeriod;
user0Cfg.appServiceInfo->timerMaxMillisecond = ICall_getMaxMSecs();
#endif /* ICALL_JT */
/* Initialize ICall module */
ICall_init();
/* Start tasks of external images - Priority 5 */
ICall_createRemoteTasks();
/* Kick off profile - Priority 3 */
GAPCentralRole_createTask();
/* Kick off application - Priority 1 */
SimpleBLECentral_createTask();
/* enable interrupts and start SYS/BIOS */
BIOS_start();
return 0;
}
程序簡單明瞭,前面是初始化,後面是任務建立,最終開始跑系統。
眼尖的朋友們應該看到這行代碼了:
// PIN_init(BoardGpioInitTable);
PIN_init(UserGpioInitTable);
是的,協議棧原來的IO口初始化被我替換了!很多朋友問,我自己畫的圖和開發板的線路不一樣,我要把串口放到哪個IO口,IIC放在哪個IO口,應該怎麼修改啊!很簡單啊,自己建立個IO口初始化表就行了啊!IO口想怎麼分配就怎麼分配。
這裏有一個問題,就是在main裏有IO口的初始化,在工程任務裏,又有個IO口初始化,到底是什麼區別?在這裏說一下,main裏的IO口初始化,主要是賦予一個上電的初始狀態,比如有些電路要求上電高電平,有些要求低電平,有些要求懸浮狀態等等,那麼就在main裏進行狀態初始化,在任務裏繼續功能初始化。
我是把自己對IO口的定義表放在了自己新建的一個文件下,IO口的初始化,都是放在這個文件下,就是下圖的UserIO.c和.h,這個文件就是對IO口進行處理的,而且以後所有的工程,這個文件只要拷貝到對應工程workspace下的APP文件夾下即可修改移植,非常方便,不同的工程,不同的IO配置,只需要在自己的文件裏進行修改就可以了,互不干涉。
const PIN_Config UserGpioInitTable[] = {
USER_UART_RX_PIN | PIN_INPUT_EN | PIN_PUSHPULL, /* UART RX via debugger back channel */
USER_UART_TX_PIN | PIN_GPIO_OUTPUT_EN | PIN_GPIO_HIGH | PIN_PUSHPULL, /* UART TX via debugger back channel */
PIN_TERMINATE
};
由於我這個項目,沒用到任何IO口,所以就配置了一下串口。這個是初始化上電的狀態。
我們知道一個項目,都是對應一個硬件的,所以IO口的初始化是開始最重要的一環,只有軟硬相匹配,下一步調試起來更方便。
接下來非常重要的一環,就是顯示打印,做嵌入式,所有的程序都在MCU裏跑,怎麼查問題,及怎麼測試自己程序的正確性,就是實現可視化(說一句題外話,可視化是一個大方向,還是未來大有可爲的一個方向,人對世界最主觀的認識,就是靠視覺,你能把什麼不可見的信號,轉爲可視化,做的好就是諾貝爾級的成就。比如熱傳感器,就是將熱量的分佈,進行可視化,這樣是不是很通俗易懂?2017年的諾貝爾獎,就是冷凍電子顯微鏡,就是可視化的一大成就。),我們調試程序,如果能實現串口打印,那對我們整個效率是個非常大的幫助,可以查看關鍵的寄存器,執行的狀態等等,所以我們下面要講的, 就是串口的設置,推進項目進展的一大利器!
TI的協議棧工程,都是自帶串口打印顯示的,但是有幾個缺陷,一是IO分配固定,要改就是改底層,二是一些子程序不透明,三,就是需要專門的串口軟件才能正確讀出。優點麼,就是可以直接用,方便,一些字符轉換的子程序都是現成。我傾向於自己寫串口程序,只要寫了一次,以後所有的程序全部可以移植通用,一次付出,高額回報。而且很多人的工程應用本身就要用到串口,一次性解決。
使用自己寫的串口,首先要把協議棧自帶的給屏蔽,方法很簡單,就是在預定義裏,把BOARD_DISPLAY_USE_LCD ,BOARD_DISPLAY_USE_UART ,BOARD_DISPLAY_USE_UART_ANSI這幾個定義全部等於0就可以了。如下圖。
然後自己建一個串口文件,我有上傳在香瓜的CC2640R2F的QQ羣裏,大家可以下來稍作修改,直接使用。這個文件,寫好後,可以隨便移植,各個不同的工程裏各自設置參數,IO口,簡單方便。現在講解下這個串口程序。
UARTCC26XX_Object UseruartCC26XXObjects[CC2640R2_LAUNCHXL_UARTCOUNT];
const UARTCC26XX_HWAttrsV2 UseruartCC26XXHWAttrs[CC2640R2_LAUNCHXL_UARTCOUNT] = {
{
.baseAddr = UART0_BASE,
.powerMngrId = PowerCC26XX_PERIPH_UART0,
.intNum = INT_UART0_COMB,
.intPriority = ~0,
.swiPriority = 0,
.txPin = USER_UART_TX_PIN,
.rxPin = USER_UART_RX_PIN,
.ctsPin = PIN_UNASSIGNED,
.rtsPin = PIN_UNASSIGNED
}
};
const UART_Config UserUART_config[CC2640R2_LAUNCHXL_UARTCOUNT] = {
{
.fxnTablePtr = &UARTCC26XX_fxnTable,
.object = &UseruartCC26XXObjects[CC2640R2_LAUNCHXL_UART0],
.hwAttrs = &UseruartCC26XXHWAttrs[CC2640R2_LAUNCHXL_UART0]
},
};
由於TI寫好的底層配置程序,都是對結構體進行直接操作,所以,我們先定義這幾個結構體。最重要的看UserUART_config這個結構體,裏面包含了三個成員:第一個成員,就是底層程序(包含了初始化,發送接收等等程序)的指針,這個是TI已經寫好的,所以我們把地址賦予。第二個成員是object,這個在初始化之前是空的,就是沒有任何內容,在初始化以後,就會把程序,參數這些分配好的集合,放在這裏,在TI的底層程序裏,就是直接調用object,這個類似於歸類,就是我把幾個人,歸爲A組,那我下次就直接喊,A組,你去做什麼事情,那麼A組的幾個成員自然知道是叫他們做事情,就這麼簡單。
第三個成員是hwAttrs,這個是硬件分配,就是你要設置的對象,在上面的比喻裏,就是我要把哪幾個人分到A組,這個是要在初始化的時候非常明確的,分配好以後,在進行調用就非常方便了。我們看看hwAttrs的成員裏,就包含了硬件IO口的信息,我們把自己工程的硬件IO口,寫到這個成員裏就可以了。當然,這個define我是寫在UserIO.h裏的,因爲這個屬於IO口的分配嘛。
是不是很簡單?
接下來,我們看看初始化,都做了些什麼:
void UserUartInit(void)
{
UART_Params_init(&Uartparams); //初始化是賦予一個默認值
Uartparams.baudRate = 115200;
Uartparams.writeDataMode = UART_DATA_BINARY; //可以選擇二進制格式還是10進制格式
Uartparams.readMode = UART_MODE_CALLBACK;
Uartparams.readDataMode = UART_DATA_BINARY;
Uartparams.readCallback = UartreadCallback;
Uarthandle = UserUART_config[0].fxnTablePtr->openFxn((UART_Handle)&UserUART_config[0],&Uartparams);
// UARTCC26XX_read(UART_Handle handle, void *buffer, size_t size)
wantedRxBytes = 20;
UserUART_config[0].fxnTablePtr->readFxn(Uarthandle,UserrxBuf,wantedRxBytes);
UserUART_config[0].fxnTablePtr->controlFxn(Uarthandle, UARTCC26XX_CMD_RETURN_PARTIAL_ENABLE,NULL);
}
我們看初始化的第一條子程序UART_Params_init(),這是對一些參數給予一個默認值,這個默認值是TI預先設置的,避免一些新手,沒有完全設置到一些參數,導致程序崩潰,所以,我們先調用這個子程序,對所有參數進行賦予默認值。
接下來,我們對一些至關重要的參數進行自定義賦值,比如波特率,進制格式,接收是選擇回調呢還是等待,接收數據是什麼格式,如果是回調模式,就要設置回調程序的指針,等等,設置好了以後,進行初始化,就是這個openFxn子程序,就是把我們前面定義的結構體,他包含了硬件信息,object,等,與我們要進行設置的參數,進行分配,然後返回一個已經配置好了的handle,很多人糾結這個handle是個什麼玩意,其實這個是給MCU認的,你認人,認臉,認聲音,MCU認這些參數,就是認一個首地址及格式,handle實際就是個首地址,你配置的個個參數存放的首地址。
打開了串口以後,就要開啓串口接收的功能,如果不開啓接收,那麼就只能發送,不能接收了。
其他的,就是串口的讀寫程序,比較簡單,自己看一下羣文件裏串口的那個。
這裏有個注意點,別在串口讀回調裏進行BLE,串口的一些操作,回調程序裏儘量簡單,不對內存進行申請等操作,會直接導致程序崩潰。及其重要,要保持這個習慣,你的程序就會正常很多。就好比你寫裸機,中斷程序裏是越簡短越好,最好就是設置個操作位,到主程序去處理,回調也是一樣。很多人說自己寫RTOS容易崩潰,就是這個原因,在回調裏進行內存申請操作。
現在來看主機的程序。任務初始化不講,就是主機設備初始化,只講解主機的思路。
作爲主機非常簡單,就是掃描去發現從機,決定連接不連接從機,如果連接上了,那就獲得從機的服務,通道信息,然後進行通訊。
官方的程序,這些操作都是用按鍵來輔助完成。
簡單說下官方的流程:
設備初始化結束以後,會打印初始化完成,然後按左邊的鍵,進行掃描。
符合UUID規則的,進行保存,掃描結束後,顯示掃描到的從機,左鍵切換顯示,右鍵發起鏈接。
然後連接上以後,左鍵選擇接下來要進行的讀,寫,等不同操作,右鍵進行確認操作。
簡單來說,流程就是如此,我們來解讀一下幾個重要的程序段。
if (ICall_fetchServiceMsg(&src, &dest,
(void **)&pMsg) == ICALL_ERRNO_SUCCESS)
{
if ((src == ICALL_SERVICE_CLASS_BLE) && (dest == selfEntity))
{
// Process inter-task message
SimpleBLECentral_processStackMsg((ICall_Hdr *)pMsg);
}
if (pMsg)
{
ICall_freeMsg(pMsg);
}
}
// If RTOS queue is not empty, process app message
if (events & SBC_QUEUE_EVT)
{
while (!Queue_empty(appMsgQueue))
{
sbcEvt_t *pMsg = (sbcEvt_t *)Util_dequeueMsg(appMsgQueue);
if (pMsg)
{
// Process message
SimpleBLECentral_processAppMsg(pMsg);
// Free the space from the message
ICall_free(pMsg);
}
}
}
如上面程序所示,最主要的是兩個消息事件,這兩個消息有什麼區別?
簡單來講,SimpleBLECentral_processStackMsg這個消息,是BLE協議的信息,屬於HCI層,GATT層的信息,需要非常及時處理的,就是從其他任務(BLE的stack任務裏)傳遞過來的信息。SimpleBLECentral_processStackMsg跳到這個子程序,我們看到,他包含了主機設備狀態改變的信息,回覆消息的信息,這個回覆消息很重要,我們要知道,BLE主從通訊,是一問一答模式,就是說主機發出去一條信息,必定要收到一條從機回覆的消息,如果沒有這條小心,那麼主機就會開始判斷計時,計時超過,判斷斷開。我們執行任何讀,寫操作的回覆,都在這裏,還有從機發送的通知也是在這裏。
SimpleBLECentral_processAppMsg 這個消息,顧名思義,就是我們應用層設定的消息,例程裏,這個信息也包含了上面那個信息。應該是Stack的消息需要更緊急處理,而app的任務,是在有隊列的時候,可以延後處理。
我們現在來看消息處理裏比較關鍵的子程序SimpleBLECentral_processRoleEvent,這個裏面主要處理的形參是BLE的操作代碼,就是central的狀態,我們一次分析如下幾個操作代碼:
GAP_DEVICE_INIT_DONE_EVENT:設備初始化完成,就是我們初始化註冊了主機以後,底層完成以後,會通過這個通知應用。
GAP_DEVICE_INFO_EVENT:這個是掃描到信息的消息,只有在發起掃描後發現設備,纔會進入這裏,否則是不會進入這裏的,切記。例程裏,就是在這裏過濾從機的,只有UUID符合的,纔會保存這個從機,我們可以根據自己的需求,在這裏修改。
這個狀態很重要,我們掃描到廣播,會進來這裏,發送掃描迴應,也會進入這裏。如何判斷是廣播和還是掃描迴應?我們看這個形參:
pEvent->deviceInfo,這個形參是一個結構體,結構體的定義跳過去如下:
typedef struct
{
osal_event_hdr_t hdr; //!< @ref GAP_MSG_EVENT and status
uint8 opcode; //!< @ref GAP_DEVICE_INFO_EVENT
uint8 eventType; //!< Advertisement Type: @ref GAP_Adv_Report_Types
uint8 addrType; //!< address type: @ref GAP_Addr_Types
uint8 addr[B_ADDR_LEN]; //!< Address of the advertisement or SCAN_RSP
int8 rssi; //!< Advertisement or SCAN_RSP RSSI
uint8 dataLen; //!< Length (in bytes) of the data field (evtData)
uint8 *pEvtData; //!< Data field of advertisement or SCAN_RSP
} gapDeviceInfoEvent_t;
這個結構體裏包含了很多信息,在例程裏,只是提取了pEvtData數據和長度,其實還有好多可以使用,看大家自己的具體應用了,比如從機的藍牙地址,RSSI,還有最關鍵的,eventType,這個形參,代表了你現在獲得的是廣播數據還是掃描迴應數據。我們可以通過判斷這個結構體,進行自己想要的操作。
GAP_DEVICE_DISCOVERY_EVENT:這個是掃描完畢的狀態,兩種情況進入這裏,一是掃描的時間到了,二是你主動取消掃描,都會進入這裏。掃描完畢,我們可以在這裏進行發起鏈接的操作。
GAP_LINK_ESTABLISHED_EVENT:這個是建立連接的狀態,例程裏,是在這裏調用了發現所有從機服務的子程序,我們在這裏,也可以做一些狀態指示啊,如果有自定義的加密協議啊,可以在這裏開始一些密碼驗證工作。
GAP_LINK_TERMINATED_EVENT:斷開連接,顧名思義,連接斷開後,會在這裏進行通知。
GAP_LINK_PARAM_UPDATE_EVENT:更新鏈接參數,就是從機開啓了自動更新鏈接參數的話,發送鏈接參數更新命令上來,協議棧就會通知應用層。
這個子程序是非常重要的,他顯示了目前BLE設備的工作狀態,我們都是要根據設備的工作狀態進行相應的操作的!一定要深刻了解這一段程序。
另一個比較關鍵的子程序,就是協議棧的回覆消息處理了,這個子程序SimpleBLECentral_processGATTMsg。
這裏相對比較簡單,主要就是(pMsg->method == ATT_READ_RSP) 對這個形參進行判斷,程序非常好看懂,需要注意的是,我們要添加來自從機NOTIFY的判斷的話,就在這裏加個比較就可以了,如下圖的NOTIFY的定義:
#define ATT_PREPARE_WRITE_REQ 0x16 //!< ATT Prepare Write Request. This method is passed as a GATT message defined as @ref attPrepareWriteReq_t
#define ATT_PREPARE_WRITE_RSP 0x17 //!< ATT Prepare Write Response. This method is passed as a GATT message defined as @ref attPrepareWriteRsp_t
#define ATT_EXECUTE_WRITE_REQ 0x18 //!< ATT Execute Write Request. This method is passed as a GATT message defined as @ref attExecuteWriteReq_t
#define ATT_EXECUTE_WRITE_RSP 0x19 //!< ATT Execute Write Response. This method is passed as a GATT message defines as @ref attHandleValueNoti_t
#define ATT_HANDLE_VALUE_NOTI 0x1b //!< ATT Handle Value Notification. This method is passed as a GATT message defined as @ref attErrorRsp_t
#define ATT_HANDLE_VALUE_IND 0x1d //!< ATT Handle Value Indication. This method is passed as a GATT message defined as @ref attHandleValueInd_t
#define ATT_HANDLE_VALUE_CFM 0x1e //!< ATT Handle Value Confirmation. This method is passed as a GATT message
我這裏只截取了一部分,具體的自己看。ATT_HANDLE_VALUE_NOTI 添加一個(pMsg->method == ATT_HANDLE_VALUE_NOTI ) 的判斷,就可以收到從機的notify信息了。
主機的程序大概流程如下,這是基本思路,至於其他的應用處理,大家根據自己的項目需求,進行修改。這樣一看,主機程序是不是非常簡單?其實BLE的流程本來就是這麼簡單的,主從機結合起來看,更容易懂,不過很多人因爲工作需求,只爲了趕項目,不想從頭開始,只想快速入手進行項目,只能說,自求多福,畢竟BLE裏的小坑還是不少。
最後,希望本帖子能對大家有所幫助。
再次重申:原創博客,如有轉載,註明出處——在金華的電子民工林。
如果覺得對你有幫助,一起到羣裏探討交流。
1)友情夥伴:甜甜的大香瓜
2)聲明:喝水不忘挖井人,轉載請註明出處。
3)糾錯/業務合作:[email protected]
4)香瓜BLE之CC2640R2F羣:557278427
5)本文出處:原創連載資料《簡單粗暴學藍牙5》
6)完整開源資料下載地址:
https://shop217632629.taobao.com/?spm=2013.1.1000126.d21.hd2o8i
————————————————
版權聲明:本文爲CSDN博主「在金華的電子小民工」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/ganjielian0930/article/details/78110918