发新帖我要提问
12
返回列表
打印
[Zigbee]

CC2530协议栈剖析

[复制链接]
楼主: gwsan
手机看帖
扫描二维码
随时随地手机跟帖
21
gwsan|  楼主 | 2019-8-5 12:12 | 只看该作者 回帖奖励 |倒序浏览
1.4 ZStack 工作原理

对于基本概念有了一定了解,现在我们来看看ZStzck的工作原理。

图 1-8 Z-Stack 工作流程图

使用特权

评论回复
22
gwsan|  楼主 | 2019-8-5 12:12 | 只看该作者
Z-Stack采用操作系统的思想来构建,采用事件轮循机制,而且有一个专门的Timer2 来负责定时。从 CC2530 工作开始,Timer2 周而复始地计时,有采集、发送、接收、显示…等任务要执行时就执行。

当各层初始化之后,系统进入低功耗模式,当事件发生时,唤醒系统,开始进入中断处理事件,结束后继续进入低功耗模式。如果同时有几个事件发生,判断优先级,逐次处理事件。这种软件构架可以极大地降级系统的功耗。

整个 ZStack 的主要工作流程,如图所示,大致分为以下 6 步:

(1) 关闭所有中断;
(2) 芯片外部(板载外设)初始化;
(3) 芯片内部初始化;
(4) 初始化操作系统;
(5) 打开所有中断;
(6) 执行操作系统。

使用特权

评论回复
23
gwsan|  楼主 | 2019-8-5 12:13 | 只看该作者
其中,初始化操作系统和执行操作系统这两步是最为关键的两步。这两步都是在位于 ZMain 文件夹下的 ZMain.c 文件里的 main 函数里进行的。 main 函数如下面代码所示:

int main( void )
{
  // Turn off interrupts
  osal_int_disable( INTS_ALL ); //关闭所有中断
  // Initialization for board related stuff such as LEDs
  HAL_BOARD_INIT();             //初始化系统时钟
  // Make sure supply voltage is high enough to run
  zmain_vdd_check();            //检查芯片电压是否正常
  // Initialize board I/O
  InitBoard( OB_COLD );         //初始化I/O ,LED 、Timer 等
  // Initialze HAL drivers
  HalDriverInit();              //初始化芯片各硬件模块
  // Initialize NV System
  osal_nv_init( NULL );         //初始化Flash 存储器
  // Initialize the MAC
  ZMacInit();                   //初始化MAC 层
  // Determine the extended address
  zmain_ext_addr();             //确定IEEE 64位地址
  // Initialize basic NV items
  zgInit();                     //初始化非易失变量
#ifndef NONWK
  // Since the AF isn't a task, call it's initialization routine
  afInit();
#endif
  // Initialize the operating system
  osal_init_system();           //初始化操作系统
  // Allow interrupts
  osal_int_enable( INTS_ALL );  //使能全部中断
  // Final board initialization
  InitBoard( OB_READY );        //最终板载初始化

  // Display information about this device
  zmain_dev_info();             //显示设备信息
  /* Display the device info on the LCD */
#ifdef LCD_SUPPORTED
  zmain_lcd_init();             //初始化LCD
#endif
#ifdef WDT_IN_PM1
  /* If WDT is used, this is a good place to enable it. */
  WatchDogEnable( WDTIMX );
#endif
  osal_start_system(); // No Return from here 执行操作系统,进去后不会返回
  return 0;  // Shouldn't get here.
}

使用特权

评论回复
24
gwsan|  楼主 | 2019-8-5 12:13 | 只看该作者
然后,我们就进入 osal_init_system()函数(初始化操作系统),其代码如下所示:

uint8 osal_init_system( void )
{
// Initialize the Memory Allocation System
osal_mem_init(); //初始化存储器

// Initialize the message queue
osal_qHead = NULL; //初始化消息队列

// Initialize the timers
osalTimerInit(); //初始化定时器

// Initialize the Power Management System
osal_pwrmgr_init(); //初始化电源管理系统

// Initialize the system tasks.
osalInitTasks(); //初始化任务

// Setup efficient search for the first free block of heap.
osal_mem_kick(); //设置第一个空的堆栈为高效搜索

return ( SUCCESS ); //返回初始化成功
}

使用特权

评论回复
25
gwsan|  楼主 | 2019-8-5 12:47 | 只看该作者
osal_init_system()函数里面是一些初始化操作,我们需要关心的是 osalInitTasks()函
数,至于其他的初始化函数,都是关于芯片正常工作所需要的配置,所以,用户可以不用考虑。osalInitTasks()函数的代码如下所示:

void osalInitTasks( void )
{
uint8 taskID = 0;

// 分配内存,返回指向缓冲区的指针
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);

// 设置所分配的内存空间单元值为 0
osal_memset(tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));



//任务优先级由高向低依次排列,高优先级对应 taskID的值反而小
macTaskInit( taskID++ ); //macTaskInit(0) ,用户不需考虑
nwk_init( taskID++ ); //nwk_init(1),用户不需考虑
Hal_Init( taskID++ ); //Hal_Init(2) ,用户需考虑
#if defined( MT_TASK )
MT_TaskInit(taskID++ );
#endif
APS_Init( taskID++ ); //APS_Init(3) ,用户不需考虑
#if defined ( ZIGBEE_FRAGMENTATION )
APSF_Init(taskID++ );
#endif
ZDApp_Init( taskID++ ); //ZDApp_Init(4) ,用户需考虑
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT)
ZDNwkMgr_Init(taskID++ );
#endif
SampleApp_Init(taskID ); // SampleApp_Init _Init(5) ,用户需考虑

}


osalInitTasks()函数一眼看上去,好像复杂,其实,这个函数就是做了一些任务的初始化,然后用 taskID 这个变量来登记任务。从上面代码可以看到,有些函数后面的注解写着“用户需考虑” ,有的写着“用户不需考虑” ,为什么这样?这是因为每个使用Zstack 的用户,搭建出来的硬件平台都是不一样的,写着“用户需考虑”的函数,就是用户可以根据自己的硬件平台来设置;写着“用户不需考虑”的函数,就是大家都是同样的配置就可以了,TI 已经为用户配置好了,所以用户就不用去考虑了。

在 osalInitTasks()函数里,有 MAC 层、网络层、硬件层…等等的任务初始化,但是我们只用关心为用户提供各层接口的用户层就可以了, 也就是说 osalInitTasks()函数的重点就在 SampleApp_Init( taskID )函数,但是这里我们先不讲,待会讲到 SampleApp.c 时再细讲。

使用特权

评论回复
26
gwsan|  楼主 | 2019-8-5 12:47 | 只看该作者
最后,我们就进入 osal_start_system()函数(执行操作系统),其代码如下所示:

void osal_start_system( void )
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;) // Forever Loop
#endif
{
uint8 idx = 0;

osalTimeUpdate();
//1、扫描哪个事件被触发了,然后置相应的标志位


Hal_ProcessPoll(); // 此函数代替了 MT_SerialPoll() and osal_check_timer().

do {
if (tasksEvents[idx]) // Task is highest priority that is ready.
{
break; //2、得到待处理的最高优先级任务索引号 idx

}
} while (++idx<tasksCnt);

if (idx<tasksCnt)
{
uint16 events;
halIntState_tintState;

HAL_ENTER_CRITICAL_SECTION(intState); // 进入临界区, 保护

events = tasksEvents[idx];
//3、提取需要处理的任务中的事件
tasksEvents[idx] = 0; // Clear the Events for this task. 清除本次任务的事件

HAL_EXIT_CRITICAL_SECTION(intState); // 退出临界区

events = (tasksArr[idx])( idx, events );
//4、通过指针调用任务处理函数

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
}
}

使用特权

评论回复
27
gwsan|  楼主 | 2019-8-5 12:48 | 只看该作者
osal_start_system()函数主要在重复地做 4 件事:
(1) 扫描哪个事件被触发了,然后置相应的标志位;
(2) 得到待处理的最高优先级任务索引号 idx;
(3) 提取需要处理的任务中的事件;
(4) 通过指针调用任务处理函数。
osal_start_system()函数是 main 函数里主循环。对于这个函数,我们只需要了解它大概的工作流程就可以了,关键还是上面提到的 SampleApp.c 文件。

使用特权

评论回复
28
gwsan|  楼主 | 2019-8-5 12:48 | 只看该作者
1.5 Z-stack 中如何实现自己的任务
我们学习 Z-Stack 的目的就是开发自己的应用程序,那么我们自己的应用程序是在
哪里进行编写的?其实,就是在我们上文一直提到的 SampleApp.c 文件里,下面我们就对其进行剖析。

1.5.1 SampleApp.c 文件分析
由于 SampleApp.c 的内容较多,这里就不贴出来了,具体大家可以打开工程阅读。SampleApp.c 文件里面的内容可以分为以下 4 类:

(1) 包括进去的头文件;
(2) 各种变量定义;
(3) 函数声明;
(4) 6 个函数的原型。

其中,最重要的就是 6 个函数的原型,我们往后的用户代码的编写基本上都是在这里进行,当然,hal 层的一些驱动修改,中断服务函数的编写,以及一些跟数据收发有关的宏定义(在 SampleApp.h 里进行)等是个例外。这 6 个函数,有关于初始化用户功能的;有关于所有事件的处理的;有按键服务功能编写的;有接收数据的处理的;有数据发送函数的编写的。下面我们就对这 6 个函数进行分析:

        void SampleApp_Init( uint8 task_id )
这是一个用户用于初始化任务的函数,它是在系统初始化阶段被调用的,而且,在调用它的时候,它里面应该有相应任务的初始化(也就是说,用户有什么初始化,都是放在这个函数里),比如:硬件初始化、数据表初始化、电源初始化等等。

        uint16 SampleApp_ProcessEvent( uint8 task_id, uint16 events )
这是一个事件处理函数(所有事件都是它管理),当有已登记的事件发生时,它就被主函数调用来对事件进行处理,这些事件包括:定时器、消息、以及用户定义的事件等。

这个函数的功能包括:按键处理,数据接收,数据发送。当然,用户可以自己添加其它事件,如“基于协议栈的串口透传”实验里,我们就在这个函数添加自己的串口事件。

        void SampleApp_HandleKeys( uint8 shift, uint8 keys )
这个函数是给 SampleApp_ProcessEvent( uint8 task_id, uint16 events )调用的: 当按键按下后,就会执行 SampleApp_ProcessEvent 函数里的 case KEY_CHANGE ,这个case 的服务函数就是 SampleApp_HandleKeys( uint8 shift, uint8 keys ),我们可以在这个函数里添加按键判断,判断哪个按键,然后执行相关任务,具体在“基于协议栈的按键实验”里讲解。

*        void SampleApp_MessageMSGCB( afIncomingMSGPacket_t pkt )
这个函数管理所有接收到的数据,至于数据来自哪个设备,它是根据簇 ID 来分辨的。

函数里面就是一个 switch 语句,关键是 case 及其后面的服务函数。用户可以根据不同的功能,定义不同的簇 ID(在 SampleApp.h 里进行),然后在这个 switch 语句里添加一个以簇 ID 来命名的 case,并在 case 里面编写自己的应用程序,如“基于协议栈的串口透传”实验里会有这个知识的讲解。

        void SampleApp_SendPeriodicMessage( void )
这个函数,是一个周期性数据发送函数,它是设备设置为周期性广播的时候调用的,它是我们设置发送数据的地方,我们要注意:

①SAMPLEAPP_PERIODIC_CLUSTERID //这是一个簇,定义的作用是和接收方建立联系,协调器收到这个标号,如果是 1,就证明是由周期性广播方式发送过来的。

②1 //数据长度

③(uint8*)&SampleAppPeriodicCounter //要发送的内容(指针形式)
void SampleApp_SendFlashMessage( uint16 flashTime )

这个函数是 void SampleApp_HandleKeys( uint8 shift, uint8 keys )函数的一个服务函数,我们也是极少用到这个函数的,故不作详解

使用特权

评论回复
29
gwsan|  楼主 | 2019-8-5 13:00 | 只看该作者
1.5.2 ZStack 的数据传输关系
我们使用 Zigbee 的目的就是为了使用它强大的数据收发功能, 那么我们有必要分析它的数据传输关系,也就是分析:数据发送函数的数据从哪里来、以及数据接收函数的数据从哪里来的问题。

1.5.3数据发送函数的数据从哪里来
要分析数据发送函数的数据从哪里来,就得研究一下数据发送函数本身,下面是数据发送函数(周期性广播)的代码:

void SampleApp_SendPeriodicMessage( void )
{
        if( AF_DataRequest( &SampleApp_Periodic_DstAddr, &SampleApp_epDesc,
                                         SAMPLEAPP_PERIODIC_CLUSTERID,
                                        1,
                                        (uint8*)&SampleAppPeriodicCounter,
                                        &SampleApp_TransID,
                                        AF_DISCV_ROUTE,
                                        AF_DEFAULT_RADIUS ) == afStatus_SUCCESS )
        {
        }
        else
        {
                // Error occurred in request to send.
        }
}

在上文讲解 SampleApp.c 的时候,我们讲解过这个函数。实际上这个函数是通过调用 AF_DataRequest 函数来进行数据发送的,而 AF_DataRequest 函数里面的一个形参 (uint8*)&SampleAppPeriodicCounter,就是指向用户数据(需要发送的数据)的指针。比如,我们要发送数组 Data[]={0,1,2,3,4,5,6,7,8,9};里的数据, 则只要用数组的首地址 Data 将形参(uint8*)&SampleAppPeriodicCounter 替换掉即可。


使用特权

评论回复
30
gwsan|  楼主 | 2019-8-5 13:00 | 只看该作者
1.5.4数据接收函数的数据从哪里来
下面是接收函数的代码:

void SampleApp_MessageMSGCB(afIncomingMSGPacket_t *pkt )
{
        uint16 flashTime;

        switch ( pkt->clusterId )
        {
                case SAMPLEAPP_PERIODIC_CLUSTERID:
                 break;

                 case SAMPLEAPP_FLASH_CLUSTERID:
            flashTime = BUILD_UINT16(pkt->cmd.Data[1], pkt->cmd.Data[2] );
            HalLedBlink( HAL_LED_4, 4, 50, (flashTime / 4) );
                break;
        }
}

使用特权

评论回复
31
gwsan|  楼主 | 2019-8-5 13:01 | 只看该作者
从上面的代码可以看到 SAMPLEAPP_PERIODIC_CLUSTERID,这个就是我们发送函数里用到的簇,当判断到这个簇是我们预先设置好的标号,所有接收到的数据就会存 入 afIncomingMSGPacket_t *pkt 所 指 向 的 存 储 区 域 内 。 我 们 进 入afIncomingMSGPacket_t,发现它是一个结构体,如下代码所示。

typedef struct
{
        osal_event_hdr_thdr; /* OSAL Message header */
        uint16 groupId; /* Message's group ID - 0 if not set */
    uint16 clusterId; /* Message's cluster ID */
    afAddrType_tsrcAddr; /* Source Address, if endpoint is STUBAPS_INTER_PAN_EP,it's an InterPAN message */

        uint16 macDestAddr; /* MAC header destination short address */
        uint8 endPoint; /* destination endpoint */
        uint8 wasBroadcast; /* TRUE if network destination was a broadcast address */
       
        uint8 LinkQuality; /* The link quality of the received data frame */
        uint8 correlation; /* The raw correlation value of the received data frame */
        int8 rssi; /* The received RF power in units dBm */
        uint8 SecurityUse; /* deprecated */
        uint32 timestamp; /* receipt timestamp from MAC */
        afMSGCommandFormat_t cmd; /* Application Data */
}afIncomingMSGPacket_t;

使用特权

评论回复
32
gwsan|  楼主 | 2019-8-5 13:01 | 只看该作者
从上面的代码可以看到,结构体 afIncomingMSGPacket_t 有很多多的成员,那么究竟哪个是存放数据的?我们先往下面看,看到/* Application Data */没有?ApplicationData 就是用户数据的意思,那么数据就在里面,但是 afMSGCommandFormat_t 也是一个结构体, 所以我们再进去看看, 结构体 afMSGCommandFormat_t 的代码如下所示:

typedef struct
{
        byte TransSeqNumber;
        uint16 DataLength; // Number of bytes in TransData
        byte *Data; //接收到的数据的指针---by Cavani
}afMSGCommandFormat_t;

终于找到了,接收到的数据就用 Data 这个指针来获取的。其实,我们可以看到 SAMPLEAPP_FLASH_CLUSTERID 这个 case,它里面就告诉了我们使用接收到的数据的方法了:pkt->cmd.Data[1]

使用特权

评论回复
33
gwsan|  楼主 | 2019-8-5 13:01 | 只看该作者
1.5.5基于 ZStack 的用户程序开发
注意,我们往后的用户程序开发,基本上都是在 SampleApp.c 里进行的,所以读者要好好理解上面关于 SampleApp.c 的知识。

然后,下面以一般性的步骤,给大家讲解如何基于 ZStack 开发用户程序(具体在往后的实验里都会有详细的讲解):

步骤一:驱动移植;
步骤二:打开工程,在 application 文件夹下添加,并在 SampleApp.c 里将驱动的头文件包括进去;
步骤三:在 SampleApp_Init( uint8 task_id )函数里进行初始化;
步骤四:修改数据发送函数;
步骤五:修改数据接收函数。

使用特权

评论回复
34
gwsan|  楼主 | 2019-8-5 13:02 | 只看该作者
附:ZStack 协议里相关名称解释
AIB 应用支持层的信息库
AF 应用框架
APDU 应用支持子层协议数据单元
APL 应用层
APS 应用支持子层
APSDE 应用支持子层数据实体
APSDE-SAP 应用支持子层数据实体-服务接入点
APSME 应用支持子层管理实体
APSME-SAP 应用支持子层管理实体—服务接入点
ASDU APS 服务数据单元
BRT 广播重试计时器
BTR 广播事务记录
BTT 广播事件务表
CCM* CBC-MAC 模式增强计数器选项
CSMA-CA 载波侦听多路访问——冲突检测
EPID 扩展 PAN ID
FFD 全功能设备
GTS 同步时隙
HDR 头
IB 信息库
LQI 链路质量指标
LR-WPAN 低速率无线个人区域网
MAC 媒体访问控制
MCPS-SAP 媒体访问控制公用部分子层—服务接入点
MIC 消息完整性代码
MLME-SAP 媒体访问控制子层管理实体—服务接入点
MSC 消息序列图
MSDU 介质访问控制子层服务数据单元
MSG 信息服务类型
NBDT 网络广播传送时间
NHLE 上层实体
NIB 网络层信息库
NLDE 网络层数据实体
NLDE-SAP 网络层的数据实体—服务接入点
NLME 网络层管理实体
NLME-SAP 网络层管理实体—服务接入点
NPDU 网络层协议数据单元
NSDU 网络服务数据单元
NWK 网络
OSI 开放式系统互连
PAN 个人区域网络
PD-SAP 物理层数据—服务接入点
PDU 协议数据单元
PHY 物理层
PIB 个人区域网络信息库
PLME-SAP 物理层管理实体—服务接入点
POS 个人操作空间
QOS 服务质量
RFD 精简功能设备
RREP 路由应答
RREQ 路由请求
RN 路由节点
SAP 服务接入点
SKG 密钥生成
SKKE 对称密钥的密钥建立
SSP 安全服务提供商
SSS 安全服务规范
WPAN 无线个人区域网
XML 可扩展标记语言
ZB ZigBee
ZDO ZigBee 设备对象

使用特权

评论回复
35
phosphate| | 2019-8-6 14:41 | 只看该作者
感谢分享!学习一下

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则