首先声明,我是纯粹的菜鸟,看zstack有三周了。学习过程中经常来飞比论坛汲取营养。昨天读了xingqing的大作《RemoTI原版按键程序分析》(
http://www.feibit.com/bbs/viewthread.php?tid=393
),感觉获益匪浅,在他的那篇帖子的基础上,我又自己将思路梳理了一遍,将回调函数这一部分的东西先写出来,算是站在xingqing肩膀上写的吧,呵呵。感谢飞比论坛,感谢这些热心分享学习经验的人!
在看zstack的时候,一开始不懂函数指针和回调函数的概念,一直理解不了事件处理过程,都怪自己的C语言功底太差。即使找到了 int main(void)中函数 osal_start_system(); 中的事件处理过程,还是看不懂。osal_start_system();函数源代码中有这样的句子:
HAL_ENTER_CRITICAL_SECTION(intState);
events = tasksEvents[idx];
tasksEvents[idx] = 0; // Clear the Events for this task.
HAL_EXIT_CRITICAL_SECTION(intState);
events = (tasksArr[idx])( idx, events );
HAL_ENTER_CRITICAL_SECTION(intState);
tasksEvents[idx] |= events; // Add back unprocessed events to the current task.
HAL_EXIT_CRITICAL_SECTION(intState);
其中最关键的应该是那句红的了。一开始,我以为这是一个普通的数组赋值,就把它给略过去,重点理解中断的开关,把最重要的给错过了。后来觉得不对劲,才来看这个类型:tasksArr[idx],追踪这个类型的定义:
const pTaskEventHandlerFn tasksArr[] = {
macEventLoop,
nwk_event_loop,
Hal_ProcessEvent,
#if defined( MT_TASK )
MT_ProcessEvent,
#endif
APS_event_loop,
#if defined ( ZIGBEE_FRAGMENTATION )
APSF_ProcessEvent,
#endif
ZDApp_event_loop,
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
ZDNwkMgr_event_loop,
#endif
SampleApp_ProcessEvent
};
以追踪才发现,这个数组里面的个个元素都是函数!!尤其这个 SampleApp_ProcessEvent相信很多人都很熟悉了吧。这个数组的定义在OSAL_SampleApp.c文件中,在这个数组的下面紧挨着一个函数:
void osalInitTasks( void )
{
uint8 taskID = 0;//任务ID从0开始
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));//初始化为0
macTaskInit( taskID++ );
nwk_init( taskID++ );
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
SampleApp_Init( taskID );//这些函数的顺序是固定的,与上面pTaskEventHandlerFn tasksArr[]中函数的顺序一致
}
看出点端倪了吧,上面的数组包含的函数指针顺序与下面初始化函数调用这些函数的顺序是一致的!也就是说系统在初始化的时候就把这些任务都进行了注册,并且挨个执行初始化。之前一直有个疑虑:系统是怎么知道各个任务的ID是多少的呢?在哪里定义的?看到这里就想明白了,系统在进行初始化的时候,将最开始的taskID设置为0,然后按照任务的注册顺序,将各个任务的ID确定,完成了任务ID的定义。(这里有的任务,在发生时都可以被执行,那么如果是自己想要加进去的任务呢?直接加到这里,然后到事件处理函数中加上相应的代码,就可以实现任务的添加了??这个想法只是突然出现的,没有得到验证,下一步会做个实现,专门验证一下)
话题扯远了,其实我想谈的是事件在发生时,系统如何实现回调,现在回到正题。来看const pTaskEventHandlerFn tasksArr[],其类型是pTaskEventHandlerFn,回调函数到底是怎么定义的,不用说肯定要从这个类型出发进行追踪。在文件OSAL_Task.h中有这样的定义:
typedef unsigned short (*pTaskEventHandlerFn)( unsigned char task_id, unsigned short event );
这是一个典型的函数指针!C语言学的太烂,翻C语言书。想搞明白的就是:什么是回调函数?为什么要用回调函数?怎么用?
回调函数:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。也就算说用一个指针指向要调用的函数,每次调用这个函数的时候,只要使用这个指针就可以实现。
为什么要用?试想在zstack中有许多的事件,例如,某个时刻,用户按下了按键,或某个时刻Timer溢出了,或某时刻出现了数据接收中断。。。这些事件都是事先不能预料的。那么我们在写任务处理函数的时候应该怎么写这些任务处理函数?像我这种菜鸟肯定会想:把所有的函数都写出来,罗列在一个总的事件处理函数ProcessEvent中,在这个函数中写一堆switch/case,当出现事件的时候,就就挨个判断是哪个时间,然后处理。其实这种思路是对的,但做法未免难以hold住写程序的高手。他们会采用更高级的方法实现:回调函数,首先定义一个函数指针,这个指针在定义是并没有指向任何函数,不妨初始化为NULL,然后把这个指针作为事件处理函数ProcessEvent的形参,当出现具体的事件时,事件处理函数根据taskID就可以自动调用相应的函数。
现在来看一个按键任务的执行过程,其初始化过程在 HalDriverInit ();函数中,追踪,发现这个函数中有以下语句:
#if (defined HAL_KEY) && (HAL_KEY == TRUE)
HalKeyInit();
#endif
再追踪这个函数HalKeyInit();
void HalKeyInit( void )
{
#if (HAL_KEY == TRUE)
halSavedKeys = 0;
pHal_KeyProcessFunction = NULL;
#endif
}
这个函数很简单,其中的pHal_KeyProcessFunction就是一个回调函数,这里将回调函数初始化为空。这个东东是什么类型呢,再追踪:
static halKeyCBack_t pHal_KeyProcessFunction;
halKeyCBack_t这个类型又是什么?
typedef void (*halKeyCBack_t) (uint8 keys, uint8 state);
终于找到了这个函数指针的定义,这就是一个典型的函数指针!前面已经出现了一个类似的指针函数,我其实还是不熟悉,所以还要回头看C语言教程。。。
在按键发送程序中,void main()函数里面在init_system之后,有一个HalKeyConfig函数,这个函数非常重要,是对key的配置函数。
void HalKeyConfig( bool interruptEnable, halKeyCBack_t cback)
{
#if (HAL_KEY == TRUE)
Hal_KeyIntEnable = interruptEnable;//使能中断
pHal_KeyProcessFunction = cback;//注册回调函数
HAL_KEY_INT_INIT();//初始化
if ( Hal_KeyIntEnable )
{
HAL_ENABLE_KEY_INT();//如果需要使能中断,就把中断使能
osal_stop_timerEx( Hal_TaskID, HAL_KEY_EVENT);//如果有一个活动的task,就将轮询取消
}
else
{
HAL_DISABLE_KEY_INT();//否则取消中断使能
osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100);//已经取消了中断,只能进行轮询了,每100ms查询一次
}
#endif /* HAL_KEY */
}
前面已经说过,在init_system之后,有一个HalKeyConfig函数,其调用具体形式为:
HalKeyConfig(RSA_KEY_INT_ENABLED, RSA_KeyCback);
第一个参数看出确实是使能中断了,第二个就是一个函数指针啦,它的功能就是检测按键变化,并调用RTI_SendDataReq( rsaDestIndex, profileId, vendorId, txOptions, len, pData);具体这个函数的代码我也没看,功能应该就是发送数据了吧。
下面看main()函数的最后一步:
void osal_start_system( void )
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;) // Forever Loop
#endif
{
uint8 idx = 0;
osalTimeUpdate(); //这里是在扫描哪个事件被触发了,然后置相应的标志位
Hal_ProcessPoll(); // This replaces MT_SerialPoll() and osal_check_timer().
/*通过这个tasksEvents可以知道是哪个层的事件发生了,接着就是调用相应的层处理
函数,在各层出来函数中还有小的事件,这就需要用events再进一步判断了*/
do {
if (tasksEvents[idx]) // Task is highest priority that is ready.
{
break;
}
} while (++idx < tasksCnt);
//判断是否有事件产生,如果有进来if
if (idx < tasksCnt)
{
uint16 events;
halIntState_t intState;//中断位状态
HAL_ENTER_CRITICAL_SECTION(intState);//保护中断现场
events = tasksEvents[idx]; //对应相应的发生的事件的数组
tasksEvents[idx] = 0; // Clear the Events for this task.清除数组中的事件
HAL_EXIT_CRITICAL_SECTION(intState); //恢复总中断
//调用相应的处理函数,返回的是未处理的事件
events = (tasksArr[idx])( idx, events );
//保护中断现场
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
}
}
如果没有事件发生,将进入死循环,有事件发生时,首先遇到的一个函数是osalTimeUpdate();在前面介绍了一堆的事件处理函数,那么zstack具体是怎么将各个发生的事件进行置位,从而实现任务的调度呢?就在这个osalTimeUpdate();函数中。这个函数中有这样的语句:
osalTimerUpdate( elapsedMSec );//这个是关键函数(其他的语句就不说了)
这个函数是关键,就是在这个函数中,定时器溢出时将需要处理的事件提取出来,并进行标志置位的,继续追踪这个函数,发现在提取出要处理的事件后(就是一系列链表操作),有以下语句:
if ( freeTimer )//提取出来的链表
{
if ( freeTimer->timeout == 0 )//定时器是向下计数的,计到0就说明溢出了
{
osal_set_event( freeTimer->task_id, freeTimer->event_flag );//如果溢出设置相应的事件
}
osal_mem_free( freeTimer );
}
其中的osal_set_event( freeTimer->task_id, freeTimer->event_flag )函数就是我们要找的地方了。看函数名就是设置事件,其具体代码:
uint8 osal_set_event( uint8 task_id, uint16 event_flag )
{
if ( task_id < tasksCnt )
{
halIntState_t intState;
HAL_ENTER_CRITICAL_SECTION(intState); // Hold off interrupts
tasksEvents[task_id] |= event_flag; // Stuff the event bit(s)将相应层的事件标志设置为有效
HAL_EXIT_CRITICAL_SECTION(intState); // Release interrupts
}
else
return ( INVALID_TASK );
return ( SUCCESS );
}
可以看到tasksEvents[task_id] |= event_flag;这就将taskEvents进行了标志,标志完成之后,相应的层就能知道发生了什么事件,然后再根据zstack的任务调度流程去执行任务,也就回到了最开始说的回调函数,这个过程实质就是:时间发生后,系统首先在void osal_start_system( void )中的osalTimeUpdate();进行时间标志的置位。置位完成后,相应的层就能发现相应的事件,根据时间的任务ID,利用回调函数的方法去执行相应的任务处理函数(或者说是事件处理函数)。
在看zstack的时候,一开始不懂函数指针和回调函数的概念,一直理解不了事件处理过程,都怪自己的C语言功底太差。即使找到了 int main(void)中函数 osal_start_system(); 中的事件处理过程,还是看不懂。osal_start_system();函数源代码中有这样的句子:
HAL_ENTER_CRITICAL_SECTION(intState);
events = tasksEvents[idx];
tasksEvents[idx] = 0; // Clear the Events for this task.
HAL_EXIT_CRITICAL_SECTION(intState);
events = (tasksArr[idx])( idx, events );
HAL_ENTER_CRITICAL_SECTION(intState);
tasksEvents[idx] |= events; // Add back unprocessed events to the current task.
HAL_EXIT_CRITICAL_SECTION(intState);
其中最关键的应该是那句红的了。一开始,我以为这是一个普通的数组赋值,就把它给略过去,重点理解中断的开关,把最重要的给错过了。后来觉得不对劲,才来看这个类型:tasksArr[idx],追踪这个类型的定义:
const pTaskEventHandlerFn tasksArr[] = {
macEventLoop,
nwk_event_loop,
Hal_ProcessEvent,
#if defined( MT_TASK )
MT_ProcessEvent,
#endif
APS_event_loop,
#if defined ( ZIGBEE_FRAGMENTATION )
APSF_ProcessEvent,
#endif
ZDApp_event_loop,
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
ZDNwkMgr_event_loop,
#endif
SampleApp_ProcessEvent
};
以追踪才发现,这个数组里面的个个元素都是函数!!尤其这个 SampleApp_ProcessEvent相信很多人都很熟悉了吧。这个数组的定义在OSAL_SampleApp.c文件中,在这个数组的下面紧挨着一个函数:
void osalInitTasks( void )
{
uint8 taskID = 0;//任务ID从0开始
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));//初始化为0
macTaskInit( taskID++ );
nwk_init( taskID++ );
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
SampleApp_Init( taskID );//这些函数的顺序是固定的,与上面pTaskEventHandlerFn tasksArr[]中函数的顺序一致
}
看出点端倪了吧,上面的数组包含的函数指针顺序与下面初始化函数调用这些函数的顺序是一致的!也就是说系统在初始化的时候就把这些任务都进行了注册,并且挨个执行初始化。之前一直有个疑虑:系统是怎么知道各个任务的ID是多少的呢?在哪里定义的?看到这里就想明白了,系统在进行初始化的时候,将最开始的taskID设置为0,然后按照任务的注册顺序,将各个任务的ID确定,完成了任务ID的定义。(这里有的任务,在发生时都可以被执行,那么如果是自己想要加进去的任务呢?直接加到这里,然后到事件处理函数中加上相应的代码,就可以实现任务的添加了??这个想法只是突然出现的,没有得到验证,下一步会做个实现,专门验证一下)
话题扯远了,其实我想谈的是事件在发生时,系统如何实现回调,现在回到正题。来看const pTaskEventHandlerFn tasksArr[],其类型是pTaskEventHandlerFn,回调函数到底是怎么定义的,不用说肯定要从这个类型出发进行追踪。在文件OSAL_Task.h中有这样的定义:
typedef unsigned short (*pTaskEventHandlerFn)( unsigned char task_id, unsigned short event );
这是一个典型的函数指针!C语言学的太烂,翻C语言书。想搞明白的就是:什么是回调函数?为什么要用回调函数?怎么用?
回调函数:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。也就算说用一个指针指向要调用的函数,每次调用这个函数的时候,只要使用这个指针就可以实现。
为什么要用?试想在zstack中有许多的事件,例如,某个时刻,用户按下了按键,或某个时刻Timer溢出了,或某时刻出现了数据接收中断。。。这些事件都是事先不能预料的。那么我们在写任务处理函数的时候应该怎么写这些任务处理函数?像我这种菜鸟肯定会想:把所有的函数都写出来,罗列在一个总的事件处理函数ProcessEvent中,在这个函数中写一堆switch/case,当出现事件的时候,就就挨个判断是哪个时间,然后处理。其实这种思路是对的,但做法未免难以hold住写程序的高手。他们会采用更高级的方法实现:回调函数,首先定义一个函数指针,这个指针在定义是并没有指向任何函数,不妨初始化为NULL,然后把这个指针作为事件处理函数ProcessEvent的形参,当出现具体的事件时,事件处理函数根据taskID就可以自动调用相应的函数。
现在来看一个按键任务的执行过程,其初始化过程在 HalDriverInit ();函数中,追踪,发现这个函数中有以下语句:
#if (defined HAL_KEY) && (HAL_KEY == TRUE)
HalKeyInit();
#endif
再追踪这个函数HalKeyInit();
void HalKeyInit( void )
{
#if (HAL_KEY == TRUE)
halSavedKeys = 0;
pHal_KeyProcessFunction = NULL;
#endif
}
这个函数很简单,其中的pHal_KeyProcessFunction就是一个回调函数,这里将回调函数初始化为空。这个东东是什么类型呢,再追踪:
static halKeyCBack_t pHal_KeyProcessFunction;
halKeyCBack_t这个类型又是什么?
typedef void (*halKeyCBack_t) (uint8 keys, uint8 state);
终于找到了这个函数指针的定义,这就是一个典型的函数指针!前面已经出现了一个类似的指针函数,我其实还是不熟悉,所以还要回头看C语言教程。。。
在按键发送程序中,void main()函数里面在init_system之后,有一个HalKeyConfig函数,这个函数非常重要,是对key的配置函数。
void HalKeyConfig( bool interruptEnable, halKeyCBack_t cback)
{
#if (HAL_KEY == TRUE)
Hal_KeyIntEnable = interruptEnable;//使能中断
pHal_KeyProcessFunction = cback;//注册回调函数
HAL_KEY_INT_INIT();//初始化
if ( Hal_KeyIntEnable )
{
HAL_ENABLE_KEY_INT();//如果需要使能中断,就把中断使能
osal_stop_timerEx( Hal_TaskID, HAL_KEY_EVENT);//如果有一个活动的task,就将轮询取消
}
else
{
HAL_DISABLE_KEY_INT();//否则取消中断使能
osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100);//已经取消了中断,只能进行轮询了,每100ms查询一次
}
#endif /* HAL_KEY */
}
前面已经说过,在init_system之后,有一个HalKeyConfig函数,其调用具体形式为:
HalKeyConfig(RSA_KEY_INT_ENABLED, RSA_KeyCback);
第一个参数看出确实是使能中断了,第二个就是一个函数指针啦,它的功能就是检测按键变化,并调用RTI_SendDataReq( rsaDestIndex, profileId, vendorId, txOptions, len, pData);具体这个函数的代码我也没看,功能应该就是发送数据了吧。
下面看main()函数的最后一步:
void osal_start_system( void )
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;) // Forever Loop
#endif
{
uint8 idx = 0;
osalTimeUpdate(); //这里是在扫描哪个事件被触发了,然后置相应的标志位
Hal_ProcessPoll(); // This replaces MT_SerialPoll() and osal_check_timer().
/*通过这个tasksEvents可以知道是哪个层的事件发生了,接着就是调用相应的层处理
函数,在各层出来函数中还有小的事件,这就需要用events再进一步判断了*/
do {
if (tasksEvents[idx]) // Task is highest priority that is ready.
{
break;
}
} while (++idx < tasksCnt);
//判断是否有事件产生,如果有进来if
if (idx < tasksCnt)
{
uint16 events;
halIntState_t intState;//中断位状态
HAL_ENTER_CRITICAL_SECTION(intState);//保护中断现场
events = tasksEvents[idx]; //对应相应的发生的事件的数组
tasksEvents[idx] = 0; // Clear the Events for this task.清除数组中的事件
HAL_EXIT_CRITICAL_SECTION(intState); //恢复总中断
//调用相应的处理函数,返回的是未处理的事件
events = (tasksArr[idx])( idx, events );
//保护中断现场
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
}
}
如果没有事件发生,将进入死循环,有事件发生时,首先遇到的一个函数是osalTimeUpdate();在前面介绍了一堆的事件处理函数,那么zstack具体是怎么将各个发生的事件进行置位,从而实现任务的调度呢?就在这个osalTimeUpdate();函数中。这个函数中有这样的语句:
osalTimerUpdate( elapsedMSec );//这个是关键函数(其他的语句就不说了)
这个函数是关键,就是在这个函数中,定时器溢出时将需要处理的事件提取出来,并进行标志置位的,继续追踪这个函数,发现在提取出要处理的事件后(就是一系列链表操作),有以下语句:
if ( freeTimer )//提取出来的链表
{
if ( freeTimer->timeout == 0 )//定时器是向下计数的,计到0就说明溢出了
{
osal_set_event( freeTimer->task_id, freeTimer->event_flag );//如果溢出设置相应的事件
}
osal_mem_free( freeTimer );
}
其中的osal_set_event( freeTimer->task_id, freeTimer->event_flag )函数就是我们要找的地方了。看函数名就是设置事件,其具体代码:
uint8 osal_set_event( uint8 task_id, uint16 event_flag )
{
if ( task_id < tasksCnt )
{
halIntState_t intState;
HAL_ENTER_CRITICAL_SECTION(intState); // Hold off interrupts
tasksEvents[task_id] |= event_flag; // Stuff the event bit(s)将相应层的事件标志设置为有效
HAL_EXIT_CRITICAL_SECTION(intState); // Release interrupts
}
else
return ( INVALID_TASK );
return ( SUCCESS );
}
可以看到tasksEvents[task_id] |= event_flag;这就将taskEvents进行了标志,标志完成之后,相应的层就能知道发生了什么事件,然后再根据zstack的任务调度流程去执行任务,也就回到了最开始说的回调函数,这个过程实质就是:时间发生后,系统首先在void osal_start_system( void )中的osalTimeUpdate();进行时间标志的置位。置位完成后,相应的层就能发现相应的事件,根据时间的任务ID,利用回调函数的方法去执行相应的任务处理函数(或者说是事件处理函数)。