CC2640R2F之central程序講解(上)

原創博客,如有轉載,註明出處——在金華的電子民工林。
當初本人寫在另外一個論壇上,現在移到這邊來。希望幫到更多人。
本文采用的是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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章