CC2640R2F之central程序讲解(上)

原创博客,如有转载,注明出处——在金华的电子民工林。
当初本人写在另外一个论坛上,现在移到这边来。希望帮到更多人。
本文采用的是CC2640R2F1.40协议栈版本。高阶版本可做参考。
做完一个central程序,就记一下流水,大致写下自己从一个工程如何从0开始做。
每个人接到一个项目任务,相信不仅仅是程序上的编写,还有整个工程的管理,这些做的好,以后自己修改方便,移植也方便,所以这次记下自己的流水账,用来抛砖引玉,供大家参考。
首先,我用的是CCS,说实话,写CC2640R2F强烈推荐使用CCS,非常方便,好用,还便于管理,移植等等。
由于CCS,对于每个工程,使用的是workspace,其实就是一个文件夹,携带了一个工程的各种配置和程序而已。对于我们使用者来说,一个workspace就是一个工程了。我的个人习惯是把所有CC2640R2F的workspace集中在一个工作盘符(在公司里是D盘)的一级子目录下,这样更便于管理,注意的一点是,所有workspace的路径,全部使用英文,使用中文字符容易出问题。如下图:
CC2640R2F之central程序讲解(上)_第1张图片
所以,开启一个新的工程,第一件事,就是在这个Group目录下,新建一个文件夹,然后修改成工程的名字,打开CCS,有个switch workspace选项,将workspace指向这个文件夹,一个新的工程开始!
注:以后所有切换工程,都是通过这个切换。
注:有过工程导入经验的,及看过香瓜写的《简单粗暴学蓝牙》的跳过以下分割线部分

CC2640R2F之central程序讲解(上)_第2张图片
建好workspace以后,就是导入工程了,**CCS的一个优点,就是原厂的协议栈,和我们写的工程是分离的,我们导入工程,只是对工程的一个复制,从安装文件夹复制到我们的workspace里而已,所以就不怕我们不小心修改到哪里了,导致没有原厂协议栈参考。**我这次是写一个central工程,那么我就导入central的工程就行了,我是默认安装协议栈在C盘的,所以协议栈在C盘的ti文件夹下,各个demo路径如下图:
CC2640R2F之central程序讲解(上)_第3张图片
好了,导入以后,基本工作就差最后一步了,就是先编译stack_library,再编译app。记住这个顺序。编译通过了,那正式开始搞程序了!

…………………………………………………………我是分割线………………………………………………………………………………
学CC2640R2F的第一个入门是软件的安装与编译,第二个入门就是程序从哪里开始读。只要搞清楚这两点,后边的就是C语言程序阅读的事了,对于老鸟菜鸟,都是本职工作,相信只要时间花下去,都能有收获。
那么现在来说说程序从哪开始读,对于新手很重要。大家都知道,所有的程序的都是从main开始,新手最头痛的就是main在哪,现在指给大家:
CC2640R2F之central程序讲解(上)_第4张图片
非常好找,就在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配置,只需要在自己的文件里进行修改就可以了,互不干涉。
CC2640R2F之central程序讲解(上)_第5张图片

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之central程序讲解(上)_第6张图片
然后自己建一个串口文件,我有上传在香瓜的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

你可能感兴趣的:(CC2640R2F)