本系列文章由江山(csdn名:补不补布)(github:jianggogogo)自己写成,当中用到引用时都已经标记出来,如果出现版权问题,请直接联系我修改。当然,技术在于分享,欢迎大家转载,不过请注明出处。最后,如果出现有错误的地方欢迎大家指正。
本篇文章,基于谷雨的开发手册,主要是在原来的基础上加入自己的理解:
看一看cc2640R2f的代码框架,从大体上来看函数到底是怎么样子一个结构。
1、蓝牙的笔记
作为入口函数,首先找到位置,位于startup下面的main.c:
/*******************************************************************************
int main()
{
/*注册断言回调函数*/
RegisterAssertCback(AssertHandler);
/*初始化硬件gpio口*/
PIN_init(BoardGpioInitTable);
/* Initialize ICall module */
/*初始化软件模块,用于app和stack之间的通信*/
ICall_init();
/*创建任务*/
ICall_createRemoteTasks();
/*GAPRole管理蓝牙设备角色等事务,*/
GAPRole_createTask();
/*最后创建的任务是app ,也就是 SimpleBLEPeripheral Task*/
SimpleBLEPeripheral_createTask();
/* enable interrupts and start SYS/BIOS */
BIOS_start();
return 0;
}
/*********************************************************************
static void SimpleBLEPeripheral_taskFxn(UArg a0, UArg a1)
{
//app任务初始化函数,为任务分配服务
SimpleBLEPeripheral_init();
// Application main loop
for (;;)
{
//一个用于任务的循环
}
}
App 任务初始化函数一般需要做下列几件事情:
第 65 页 / 共 177 页
ICall_registerApp注册,必须首先调用
设置 GAP 层 ,例如广播间隔等参数
设置 GAPRole ,例如是否开启广播等、连接参数等
设置绑定管理器,例如是否开启绑定,以及绑定模式。
添加 Profile ,并注册 Profile 回调函数
启动设备
其他硬件配置
uint32_t events;
// 阻塞在这里,等待事件返回
events = Event_pend(syncEvent, Event_Id_NONE, SBP_ALL_EVENTS, ICALL_TIMEOUT_FOREVER);
//不断去处理到达的事件
if (events) { }
当蓝牙协议栈通过
触发一个 RTOS 任务 ,开始任务处理 过程 。任务处理过程一般分为三个
部分。
协议栈消息,例如发送 AttRsp 确认消息 ,协议栈内部消息 GATT_MSG_EVENT 等
RTOS 消息队列,通过消息队列缓存的延期待执行的数据,例如缓存的 Profile 特征值
数据等。
自定义 EVENT ,例如 app 自定义的周期性时间,每个 5s 发送一次 notify 等。
蓝牙协议栈通过 ICall 将消息传递给应用程序任务,例如上一小节的 GATT_MSG_EVENT 消除处理函数 SimpleBLEPeripheral_processStackMsg 。
这些消息使用
SimpleBLEPeripheral_enqueueMsg
函数入队 。消息被添加到队列中,并且
按照添加的顺序进行消息处理。 发送消息和 处理消息的代码片段如下 。
uint8_t Util_enqueueMsg(Queue_Handle msgQueue, Event_Handle event, uint8_t *pMsg)
{ ...
pRec->pData = pMsg; // 队列是原子操作
Queue_put(msgQueue, &pRec->_elem); // Wake up the application thread event handler.
if (event) {
Event_post(event, UTIL_QUEUE_EVENT_ID);
}
...
return FALSE; }
在app 任务内部,可以创建 16 个自定义事件,可以通过定时 或其他方式 来异步执行事件
例如、蓝牙状态回调:
static void SimpleBLEPeripheral_processStateChangeEvt(gaprole_States_t newState) { switch ( newState ) {
case GAPROLE_STARTED:
{
//协议栈启动运行
GAPRole_GetParameter(GAPROLE_BD_ADDR, ownAddress);
}
break;
case GAPROLE_ADVERTISING:
//设备正在广播
break;
case GAPROLE_CONNECTED:
{
//设备已连接
}
break;
注意,所有的回调函数的代码处理主体应该在任务上下文中进行,也就是通过
RTOS 消息
队列的方式,先缓存起来,然后待任务空闲时处理,例如刚才的函数,是在回调函数中 push
到消息队列中。
蓝牙数据的发送就是分为两种情况:一种是主机向从机发送数据,另一种是从机向主机发送数据:
主机向从机发送数据
GATT_WriteCharValue ()()
,该函数的代码片段如下
//申请用于发送蓝牙数据的内存
req.pValue = GATT_bm_alloc(connHandle, ATT_WRITE_REQ, 1, NULL); if ( req.pValue != NULL )
{ //从机特征值uuid对应的handle,主机发起连接时获得。
req.handle = charHdl; req.len = 1; //待发送的数据
req.pValue[0] = charVal;
req.sig = 0;
req.cmd = 0;
//开始发送
status = GATT_WriteCharValue(connHandle, &req, selfEntity);
if ( status != SUCCESS )
{
GATT_bm_free((gattMsg_t *)&req, ATT_WRITE_REQ);
}
}
GATT_Notification ()()
//申请用于发送蓝牙数据的内存
noti.pValue = GATT_bm_alloc(connHandle, ATT_HANDLE_VALUE_NOTI, 2, NULL); if (noti.pValue != NULL)
{ //通知特征值的句柄,可以通过GATT属性表得到
noti.handle = pAttr->handle; noti.len = 2;
//待发送的数据
noti.pValue[0] = LO_UINT16(blkNum); noti.pValue[1] = HI_UINT16(blkNum);
//发送数据
if (GATT_Notification(connHandle, ¬i, FALSE) != SUCCESS) {
GATT_bm_free((gattMsg_t *)¬i, ATT_HANDLE_VALUE_NOTI);
}
}
和发送一样,蓝牙接受数据也是区分主机和从机两个方向。
对应GATT_WriteCharValue
函数。
第一步,注册
//Profile 回调函数,用来接收特征值事件。
// Register callback with SimpleGATTprofile
SimpleProfile_RegisterAppCBs(&SimpleBLEPeripheral_simpleProfileCBs);
第二步,在回调函数中调用
SimpleProfile_GetParameter读取 数据
static void SimpleBLEPeripheral_processCharValueChangeEvt(uint8_t paramID) {
switch(paramID)
{ case SIMPLEPROFILE_CHAR1: //读取数据
SimpleProfile_GetParameter(SIMPLEPROFILE_CHAR1, &newValue);
//打印到显示屏上
Display_print1(dispHandle, 4, 0, "Char 1: %d", (uint16_t)newValue); break; ... }
第一步,注册接收
notify 消息
// 注册接收Indications/Notifications消息
GATT_RegisterForInd(selfEntity);
第二步,
在 GATT_MSG_EVENT 消息处理函数 SimpleBLECentral_processGATTMsg增
加 ATT_HANDLE_VALUE_NOTI 处理代码 。
static void SimpleBLECentral_processGATTMsg(gattMsgEvent_t *pMsg) { if (state == BLE_STATE_CONNECTED) {
// See if GATT server was unable to transmit an ATT response
if (pMsg->hdr.status == blePending)
else if (pMsg->method == ATT_FLOW_CTRL_VIOLATED_EVENT) ...
//handle notifications after initialization
else if (pMsg->method == ATT_HANDLE_VALUE_NOTI)
{
//数据内容为:pMsg->msg.handleValueNoti.pValue //数据长度为:pMsg->msg.handleValueNoti.len
UartWrite(pMsg->msg.handleValueNoti.pValue,
pMsg->msg.handleValueNoti.len);
} ... }
蓝牙Profile 可以理解为主从双方通信过程中的格式化数据,并存储在蓝牙从机中,作为服务端,而主机作为客户端,客户端可以来获取服务端的数据或者属性,这个数据就称之为特征值。
Simple_peripheral 从机中的 Profile 为 SimpleProfile ,位于 src profiles simple_profile cc26xx
目录中。 重要的代码片段如下。
这个profile中采用的profile为:SimpleProfile。
//特征属性表
static gattAttribute_t simpleProfileAttrTbl[SERVAPP_NUM_ATTR_SUPPORTED] =
{
{
{
ATT_BT_UUID_SIZE, primaryServiceUUID
}
, /* type */
GATT_PERMIT_READ, /* permissions */
0, /* handle */
(uint8 *)&simpleProfileService /* pValue */
},
// Characteristic 1 Declaration
{
{ ATT_BT_UUID_SIZE, characterUUID }, GATT_PERMIT_READ, 0, &simpleProfileChar1Props },
// Characteristic Value 1
{
{
ATT_BT_UUID_SIZE, simpleProfilechar1UUID
},
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
&simpleProfileChar1 },
// Characteristic 1 User Description
{
{
ATT_BT_UUID_SIZE, charUserDescUUID
},
GATT_PERMIT_READ,
0,
simpleProfileChar1UserDesp
},
... };
UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,这里设置为0xFFF1。
CONST uint8 simpleProfilechar1UUID[ATT_BT_UUID_SIZE] = {
LO_UINT16(SIMPLEPROFILE_CHAR1_UUID), HI_UINT16(SIMPLEPROFILE_CHAR1_UUID)
};
用户可读的描述::
// Simple Profile Characteristic 1 User Description
static uint8 simpleProfileChar1UserDesp[17]
= "Characteristic 1";
特征值为一个字节的数据:
// Characteristic 1 Value
static uint8 simpleProfileChar1 = 0;
bStatus_t SimpleProfile_SetParameter( uint8 param, uint8 len, void *value )
{
switch
( param )
//特征值1设置
case SIMPLEPROFILE_CHAR1:
if ( len == sizeof ( uint8 ) )
{
simpleProfileChar1 = *((uint8*)value);
}
else
{
ret = bleInvalidRange;
}
break; //特征值2设置
case SIMPLEPROFILE_CHAR2:
...
break; }
bStatus_t SimpleProfile_GetParameter( uint8 param, void *value )
{ s
witch ( param )
{
case SIMPLEPROFILE_CHAR1: *((uint8*)value) = simpleProfileChar1; break;
case SIMPLEPROFILE_CHAR2:
break;
...
}
主机读取特征值
static bStatus_t simpleProfile_ReadAttrCB(uint16_t connHandle, gattAttribute_t *pAttr, uint8_t *pValue, uint16_t *pLen, uint16_t offset, uint16_t maxLen, uint8_t method)
{
if ( pAttr->type.len == ATT_BT_UUID_SIZE )
{ // 16-bit UUID
uint16 uuid = BUILD_UINT16( pAttr->type.uuid[0], pAttr->type.uuid[1]); switch ( uuid )
{
//主机读特征值策略
case SIMPLEPROFILE_CHAR1_UUID: *pLen = 1;
pValue[0] = *pAttr->pValue;
break;
case SIMPLEPROFILE_CHAR5_UUID:
*pLen = SIMPLEPROFILE_CHAR5_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR5_LEN );
break;
主机写入特征值
static bStatus_t simpleProfile_WriteAttrCB(uint16_t connHandle, gattAttribute_t *pAttr, uint8_t *pValue, uint16_t len, uint16_t offset, uint8_t method)
{ if ( pAttr->type.len == ATT_BT_UUID_SIZE )
{ // 16-bit UUID
uint16 uuid = BUILD_UINT16( pAttr->type.uuid[0], pAttr->type.uuid[1]);
switch ( uuid )
{ //主机写入数据
case SIMPLEPROFILE_CHAR1_UUID:
...
uint8 *pCurValue = (uint8 *)pAttr->pValue;
*pCurValue = pValue[0];
notifyApp = SIMPLEPROFILE_CHAR1; // 调用app注册的Profile回调函数
...
simpleProfile_AppCBs->pfnSimpleProfileChange( notifyApp );
...
bStatus_t SimpleProfile_RegisterAppCBs( simpleProfileCBs_t *appCallbacks )
{
if ( appCallbacks ) { //保存回调函数 simpleProfile_AppCBs = appCallbacks; ...
ICall是一种软件模块,可为应用程序与协议栈提供通信服务, app 中调用的协议栈 API 函数,大多来自 ICall 模块,另外 ICall 还提供 RTOS 的一些线程同步、动态内存等服务。 ICall 使得 app 和 stack 在统一的 RTOS 环境中高效运行,共享资源。
ccdc2640R2提供的存储管理叫做SNV,snv主要用于协议栈的绑定管理器存储。
开发者无需初始化SNV ,直接使用 SNV 提供的 Read/Writ e 函数即可(初始化函数已在 stack中调用,无需开发者干预)。在使用 SNV 服务之前,需要设置 stack 子工程的预处理宏定义:OSAL_SNV=1或 OSAL_SNV=2 1 或 2 表示使用的 Flash Page 4K )数量。 若 OSAL_SNV=0 ,则表示禁用 SNV 存储:
1、保存数据:
uint8 osal_snv_write( osalSnvId_t id, osalSnvLen_t len, void *pBuf)
2、读取数据:
uint8 osal_snv_read( osalSnvId_t id, osalSnvLen_t len, void *pBuf)
3、由于SNV 被多个模块共享,例如协议栈的 GapBondMgr 绑定管理器等,因此要小心的定义 snv item ,在 bcomdef.h 中查看 设置了系统占用的 item ,以及开发者可以使用的 item 范围
如下代码:
// Customer NV Items - Range 0x80 - 0x8F - This must match the number of Bonding entries
#define BLE_NVID_CUST_START 0x80 //!< Start of the Customer's NV IDs
#define BLE_NVID_CUST_END 0x8F //!< End of the Customer's NV IDs
CCA Customer Configuration Area 是客户配置取,占用闪存的最后一页。是客户配置取,占用闪存的最后一页,而 CCFG 占用CCA 扇区最后的 86 个字节。默认情况下,连接器会将 CCA 页中未使用的 Flash 空间分配给 app使用。
内存可以分为两个部分,堆和栈。函数中申请的局部变量,以及函数嵌套,中断都是使用栈空间。而静态变量和全局变量则是使用的堆空间。动态内存使用的内存也位于堆内存。
由于栈空间分配的数量十分有限,所以不要在函数中使用太大的数组。尽量使用动态内存。
sdk中一般使用 ICall_malloc 申请内存, ICall_free 释放内存 ,代码片段如下:
// 申请长度为len的动态内存
uint8 *newValue = (uint8*)ICall_malloc(len);
...
// 释放内存
ICall_free(newValue);
除了ICall_malloc 中还有另外集中申请动态内存的函数,分别是:
ICall_allocMsg ,对应 free 函数为 ICall_ free Ms g
GATT_bm_alloc ,对应 free 函数为: GATT_bm_free
三者的主要区别是,
ICall_malloc 用于开发者的一般性内存申请,而 ICall_allocMsg 主要用于 RTOS 消息队列的内存申请
GATT_bm_alloc 用于待发送的蓝牙数据内存申请。