目录
一、任务初池始化与事件处理:
(1)层次划分:
打开 GenericApp 演示项目工程后,IAR 软件左边出现如下图所示的 Z-Stack 协议栈目录结构:
其中:
组名称 | 说明 |
---|---|
App | 存放应用程序相关源代码文件。 |
BDB | 实现 ZigBee BDB(Base Device Behavior,设备基础行为)功能。 |
GP | 实现 ZigBee GP(Green Power,绿色能源)功能。 |
HAL | 硬件抽象层,存放各种驱动程序。 |
MAC | 媒体介质访问控制,实现物理层通信及 IEEE 802.15.4 协议。 |
MT | 监视层,为监视协议栈各层的运行状态提供支持。 |
NWK | ZigBee 网络层。 |
OSAL | 操作系统抽象层。 |
Profile | 存放 ZigBee 标准化定义及相关功能实现的源代码文件。 |
Security | 实现安全相关服务。 |
Services | 提供一些公共的、常用的功能。 |
Tools | 存放工程配置相关的文件。 |
ZDO | 存放 ZDO(ZigBee Device Object,ZigBee 设备对象)相关源代码文件。 |
ZMac | 属于 mac 层的内容。 |
ZMain | 存放主函数所在的源代码文件及系统硬件启动相关的源代码文件。 |
Output | 存放工程编译/链接时输出的文件。 |
Z-Stack可以被分成多个层次。
(2)任务池初始化:
每一个层次都有一个对应的任务来处理本层次的事务,例如MAC层对应一个MAC层的任务、网络层对应一个网络层的任务、HAL对应一个HAL的任务,以及应用层对应一个应用层的任务等,这些各个层次的任务构成一个任务池,这个任务池也就是tasksEvents数组。
可以在App目录下的OSAL_SampleSw.c文件中找到任务池初始化函数osalInitTasks()。
/*********************************************************************
* @fn osalInitTasks
*
* @brief This function invokes the initialization function for each task.
*
* @param void
*
* @return none
*/
void osalInitTasks( void )
{
uint8 taskID = 0;
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
macTaskInit( taskID++ );
nwk_init( taskID++ );
#if !defined (DISABLE_GREENPOWER_BASIC_PROXY) && (ZG_BUILD_RTR_TYPE)
gp_Init( taskID++ );
#endif
Hal_Init( taskID++ );
#if defined( MT_TASK )
MT_TaskInit( taskID++ );
#endif
APS_Init( taskID++ );
#if defined ( ZIGBEE_FRAGMENTATION )
APSF_Init( taskID++ );
#endif
ZDApp_Init( taskID++ );
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
ZDNwkMgr_Init( taskID++ );
#endif
// Added to include TouchLink functionality
#if defined ( INTER_PAN )
StubAPS_Init( taskID++ );
#endif
// Added to include TouchLink initiator functionality
#if defined( BDB_TL_INITIATOR )
touchLinkInitiator_Init( taskID++ );
#endif
// Added to include TouchLink target functionality
#if defined ( BDB_TL_TARGET )
touchLinkTarget_Init( taskID++ );
#endif
zcl_Init( taskID++ );
bdb_Init( taskID++ );
zclSampleSw_Init( taskID++ );
#if (defined OTA_CLIENT) && (OTA_CLIENT == TRUE)
zclOTA_Init( taskID );
#endif
}
任务ID初始化:
uint8 taskID = 0;
定义一个taskID
变量,初始值为0,用于为每个任务分配唯一的 ID。
任务事件数组分配和初始化:
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
- 使用
osal_mem_alloc
函数为任务事件数组tasksEvents
分配内存,数组的大小为tasksCnt
(任务数量)乘以每个元素的大小(sizeof(uint16)
)。 - 使用
osal_memset
函数将分配的内存初始化为 0,确保所有任务事件初始时没有待处理的事件。
初始化各个任务:
接下来的部分依次调用各个任务的初始化函数,并为每个任务分配递增的任务 ID:
macTaskInit(taskID++); // 初始化MAC层任务
nwk_init(taskID++); // 初始化网络层任务
#if !defined (DISABLE_GREENPOWER_BASIC_PROXY) && (ZG_BUILD_RTR_TYPE)
gp_Init(taskID++); // 初始化Green Power任务(可选)
#endif
Hal_Init(taskID++); // 初始化硬件抽象层任务
#if defined( MT_TASK )
MT_TaskInit(taskID++); // 初始化MT任务(可选)
#endif
APS_Init(taskID++); // 初始化APS层任务
#if defined ( ZIGBEE_FRAGMENTATION )
APSF_Init(taskID++); // 初始化APS片段任务(可选)
#endif
ZDApp_Init(taskID++); // 初始化Zigbee设备应用层任务
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
ZDNwkMgr_Init(taskID++); // 初始化网络管理任务(可选)
#endif
// 添加TouchLink功能相关初始化(可选)
#if defined ( INTER_PAN )
StubAPS_Init(taskID++);
#endif
#if defined( BDB_TL_INITIATOR )
touchLinkInitiator_Init(taskID++);
#endif
#if defined ( BDB_TL_TARGET )
touchLinkTarget_Init(taskID++);
#endif
zcl_Init(taskID++); // 初始化Zigbee集群库任务
bdb_Init(taskID++); // 初始化基本设备行为任务
zclSampleSw_Init(taskID++); // 初始化ZCL样本开关任务
#if (defined OTA_CLIENT) && (OTA_CLIENT == TRUE)
zclOTA_Init(taskID); // 初始化OTA任务(可选)
#endif
- 每个任务的初始化函数被调用时,传入当前的
taskID
,然后taskID
自增 1,为下一个任务准备 ID。 - 通过条件编译指令
#if
,可以根据不同的配置选项来决定是否包含某些任务的初始化代码,这样可以灵活地支持不同的系统配置和功能需求。
(3)事件处理函数:
在OSAL_SampleSw.c文件中定义了一个数组,
tasksEvents中的每个任务可以包含0个或者多个待处理的事件,而这个数组类型变量pTaskEventHandlerFn是一个函数指针类型变量,用于指向事件对应的处理函数,因此这段代码定义了一个事件处理函数数组,这个数组中的每一个元素均表示某一个层次任务的事件处理函数,例如:
- MAC层任务对应的事件处理函数是macEventLoop(),它专门处理MAC层任务中的事件。
- 网络层任务对应的事件处理函数是nwk_event_loop(),它专门处理网络层任务中的事件。
- 应用层任务对应的事件处理函数是zclSampleSw_event_loop(),它专门处理应用层任中的事件。
例如,以应用层事件处理函数zclSampleSw_event_loop()为例,可以在zcl_samplesw.h文件中找到该函数的声明。
BDB层也有事件处理函数,在bdb_interface.h 文件中声明。
二、Z-Stack 事件的应用:
(1)事件的类型与编码:
每个层次的事件处理函数的参数都包含1个task id和1个events参数,例如:
应用层事件处理函数:
MAC层事件处理函数:
网络层事件处理函数:
以应用层事件处理函数为例,它的第2个参数UINT16 events表示了一个事件集合,其中包含了0个或多个待处理的事件。events是一个16位的变量,Z-Stack 3.0采用了独热码(one-hot code)的方式对事件类型进行编码。
events的分类:
- events的最高位为1时,表示系统事件集合,即events中的事件全是系统事件。
- events的最高位为0时,表示用户事件集合,即events中的事件全是用户事件。
用户事件可以由开发者自行定义其含义,以及相应的处理。
用户事件编码如下:
事件名称 | 二进制编码 | 十六进制编码 |
---|---|---|
用户事件 A | 0000 0000 0000 0001 | 0x0001 |
用户事件 B | 0000 0000 0000 0010 | 0x0002 |
用户事件 C | 0000 0000 0000 0100 | 0x0004 |
用户事件 D | 0000 0000 0000 1000 | 0x0008 |
用户事件 E | 0000 0000 0001 0000 | 0x0010 |
用户事件 F | 0000 0000 0010 0000 | 0x0020 |
用户事件 G | 0000 0000 0100 0000 | 0x0040 |
用户事件 H | 0000 0000 1000 0000 | 0x0080 |
用户事件 I | 0000 0001 0000 0000 | 0x0100 |
用户事件 J | 0000 0010 0000 0000 | 0x0200 |
用户事件 K | 0000 0100 0000 0000 | 0x0400 |
用户事件 L | 0000 1000 0000 0000 | 0x0800 |
用户事件 M | 0001 0000 0000 0000 | 0x1000 |
用户事件 N | 0010 0000 0000 0000 | 0x2000 |
用户事件 O | 0100 0000 0000 0000 | 0x4000 |
事件编码规律:
规律1:除了用于表示系统事件或者用户事件的最高位,其他15个比特位中,只有1位为1,其他位均为0。
- 这意味着每个用户事件的编码在二进制表示中只有一个位是1,其余位都是0,这符合独热码(one-hot encoding)的特性。
- 例如,用户事件A的二进制编码是
0000 0000 0000 0001
,只有最低位是1;用户事件B的编码是0000 0000 0000 0010
,只有第二位是1,依此类推。
规律2:使用15个比特位表示15种用户事件。
- 由于每个事件的编码只有一个位为1,因此可以用15个比特位来唯一表示15种不同的用户事件。
应用规律理解事件集合:
假设有一个事件集合events
的值为0000 0000 0101 0101
,其中右起第1、3、5和7位为1。根据规律1,可以理解为事件集合events
包含了用户事件A、C、E和G。
最多可表示的用户事件数量:
根据规律2,由于使用了15个比特位,每个比特位对应一种用户事件,因此最多可以表示15种用户事件。
(2)定义用户事件:
在zcl_samplesw.h文件中,定义事件名称和对应的编码,即定义一个用户事件:
#define USER_EVENT 0x0040
位置如下:
(3)处理用户事件:
在zcl_samplesw.c文件中的应用层事件处理函数中添加相关的处理,位置如下:
由独热码可知,每一种用户事件类型编码中只有1位为1,其他位为0。USER_EVENT的事件类型编码为0x0040,其二进制数为:0000 0000 0100 0000。这个编码的右起第7为1,其余位为0。
USER_EVENT
的值是 0x0040
,其二进制表示为 0000 0000 0100 0000
,即右起第7位(从0开始计数)为1,其余位为0。通过按位与运算 events&USER_EVENT
,如果结果不为0,说明 events
的第7位为1,即 events
包含 USER_EVENT
事件。因此程序执行对应的处理代码:
printf("Hello World!\r\n");
接着,按位异或运算 events^USER_EVENT
会将 events
中第7位的1变为0,而其他位保持不变。这样,返回值表示已处理掉 USER_EVENT
后剩余的事件。
(4)触发用户事件:
先前已经定义好了事件类型和对应的处理方式了,但是需要在OSAL中触发该事件后,OSAL才会执行对应的处理代码。OSAL提供了专门的API来触发事件。展开OSAL层,可以找到OSAL_Timers.h文件,如下所示:
触发事件的API:
/*
* Set a Timer
*/
extern uint8 osal_start_timerEx( uint8 task_id, uint16 event_id, uint32 timeout_value );
task_id
(任务ID):
- 指定要触发事件的任务的ID号。
- 每个任务在系统中都有一个唯一的ID,用于标识不同的任务。
- 例如,
SampleApp_TaskID
表示某个特定任务的ID。
event_id
(事件ID):
- 表示要触发的事件类型。
- 当定时器超时时,系统会向指定任务发送该事件,任务会根据事件类型执行相应的处理函数。
- 例如,
SAMPLEAPP_SEND_PERIODIC_MSG_EVT
表示一个发送周期信息的事件。
timeout_value
(超时时间):
- 定义定时器的超时时间,单位通常为毫秒(ms)。
- 当定时器启动后,经过
timeout_value
指定的时间,系统会触发event_id
指定的事件。 - 例如,
SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT
定义了一个3秒的超时时间。
如果希望在触发事件的1s后处理刚才自定义的事件,可在应用层初始化函数zclSampleSw_Init()的末尾位置添加如下代码:
osal_start_timerEx(
zclSampleSw_TaskID,//标记本事件属于应用层任务
USER_EVENT,//标记本事件的类型
1000);//表示1000ms后处理这个事件
选择仿真调试器:
烧录代码:
三、动态内存:
(1)动态内存介绍:
在开发程序时,有时开发者无法预先确定需要多少内存来存储数据,因此希望在程序运行时根据实际情况动态地获取和释放所需的内存空间。这种可以在程序执行过程中按需申请和释放的内存空间被称为动态内存。简单来说,动态内存就是程序在运行时根据实际需求灵活分配和释放的内存,它提供了更高的灵活性,但需要开发者手动管理以避免内存泄漏等问题。
在Z-Stack 3.0.2中动态内存分配的API在OSAL目录下的OSAL_Memory.h文件中,如下图所示位置:
OSAL_Memory.c文件中
/**************************************************************************************************
* @fn osal_mem_alloc
*
* @brief 这个函数实现了OSAL(操作系统抽象层)的动态内存分配功能。
* 它从系统的堆(HEAP)中分配指定数量的字节,并返回指向分配内存块的指针。
* 如果内存分配成功,返回的指针可以用来访问分配的内存空间;
* 如果分配失败(例如内存不足),函数将返回NULL。
*
* 输入参数
*
* @param size - 需要从堆中分配的字节数,表示申请的内存大小。
*
* 输出参数
*
* 无。
*
* @return 返回一个指向分配内存块的指针,类型为void*。如果分配成功,返回有效的内存地址;否则返回NULL。
*/
void *osal_mem_alloc( uint16 size );
(2)申请与释放动态内存:
函数声明在OSAL_Memory.h文件中:
OSAL_Memory.C文件函数原型如下:
/**************************************************************************************************
* @fn osal_mem_free
*
* @brief 这个函数实现了OSAL(操作系统抽象层)的动态内存释放功能。
* 它将之前通过osal_mem_alloc函数分配的内存块释放回系统堆,以供其他部分的程序重新使用。
* 这是动态内存管理的重要组成部分,正确使用该函数可以避免内存泄漏问题。
*
* 输入参数
*
* @param ptr - 一个有效的指针,即通过osal_mem_alloc()函数分配内存时返回的指针,指向需要释放的内存块。
* 该函数会根据这个指针将对应的内存区域标记为可用状态,从而实现内存的回收。
* 注意,只能释放通过osal_mem_alloc分配的内存,否则可能导致不可预期的错误。
*
* 输出参数
*
* 无。
*
* @return 无返回值。该函数执行成功后,传入的ptr所指向的内存将不再有效,程序不应再访问该内存区域。
*/
void osal_mem_free(void *ptr);
如果内存空间不用了,需要调用这个API来释放内存空间。
(3)动态内存操作:
在申请完动态内存后,可以调用内存操作API来使用这些内存。内存操作API在OSAL.h文件中,下面这两个API是比较常用的,定义如下:
/**
* @fn osal_memcpy
*
* @brief 把内存空间的内容复制到另一个内存空间中
*
* @param void* - 目标内存空间
* @param const void GENERIC * - 源内存空间
* @param unsigned int - 复制多少个字节
*
* @return
*/
void *osal_memcpy(void*, const void GENERIC *,unsigned int);
/**
* @fn osal_memset
*
* @brief 把内存空间的值设置为指定的值
*
* @param dest - 内存空间
* @param value - 指定的值
* @param len - 把从dest起的len个字节的存储空间的值设置为value
*
* @return
*/
extern void *osal_memset( void *dest, uint8 value, int len );
(4)使用动态内存:
// 处理用户定义的事件:USER_EVENT
if (events & USER_EVENT)
{
// 定义一个字符串,内容为 "Hello World!\n"
char *str = "Hello World!\n";
// 从堆空间中申请32个字节的内存空间,用于存储数据
char *mem = osal_mem_alloc(32);
// 检查内存是否申请成功
if (mem != NULL)
{
// 使用osal_memset函数将申请的内存空间清零,确保内存初始状态干净
osal_memset(mem, 0, 32);
// 使用osal_memcpy函数将字符串str的内容拷贝到申请的内存空间中
// 拷贝的字节数由osal_strlen(str)确定,即字符串的实际长度
osal_memcpy(mem, str, osal_strlen(str));
// 使用printf函数将内存空间中的内容打印出来
// 注意:这里直接打印内存可能存在问题,因为mem中可能没有正确的字符串终止符
printf(mem);
}
// 释放之前申请的内存空间,避免内存泄漏
osal_mem_free(mem);
// 重新触发USER_EVENT事件,设置1000毫秒(1秒)后再次执行该事件处理函数
osal_start_timerEx(zclSampleSw_TaskID, USER_EVENT, 1000);
// 使用按位异或运算清除已经处理的USER_EVENT事件位
// 这样可以确保返回的events中不包含已经处理的USER_EVENT事件
// 返回值表示未处理的其他事件
return (events ^ USER_EVENT);
}