3.1 OSAL的任务调度原理

本节课将以分析Sample Switch这个例程源代码的方式讲解OSAL的任务调度原理。其中会涉及到复杂的源代码,读者暂时只需要大致地了解整个任务调度过程就可以了

OSAL简介

OSAL(Operating System Abstraction Layer,系统抽象层),可以通俗地理解为一个简化版的操作系统,为Z-Stack的正确运行提供了内存管理、中断管理和任务调度等基本功能。

理解任务调度过程

OSAL的任务调度其实是与上节课笔者实现的任务调度是类似的,也就是初始化任务池以及轮询任务池。

打开本节课配套的工程代码,如图所示。
第3章:OSAL 详解 - 图1
读者会发现配套的工程代码与之前的Z-Stack不同,这是因为为了方便读者学习,笔者已经把部分用不到文件给裁剪掉了。

打开SampleSwitch.eww工程文件所在的目录,如图所示。
第3章:OSAL 详解 - 图2
双击打开SampleSwitch.eww文件,打开后如图所示。
第3章:OSAL 详解 - 图3

程序一般是从main()函数开始的,Z-Stack 3.0 也不例外。它的main()函数在ZMain目录下的ZMain.c文件中,该文件在如图所示位置。
第3章:OSAL 详解 - 图4

打开ZMain.c文件,可以找到main()函数,其代码如下:

  1. 1.int main( void )
  2. 2.{
  3. 3. // Turn off interrupts
  4. 4. osal_int_disable( INTS_ALL ); // 关闭所有中断
  5. 5.
  6. 6. // Initialization for board related stuff such as LEDs
  7. 7. HAL_BOARD_INIT(); // 初始化板载资源,比如PA、时钟源等
  8. 8.
  9. 9. // Make sure supply voltage is high enough to run
  10. 10. zmain_vdd_check(); // 检测供电电压是否可以支撑芯片正常运行
  11. 11.
  12. 12. // Initialize board I/O
  13. 13. InitBoard( OB_COLD ); // 初始化板载I/O,比如按键配置为输入
  14. 14.
  15. 15. // Initialze HAL drivers
  16. 16. HalDriverInit(); // 初始化硬件适配层,比如串口、显示器等
  17. 17.
  18. 18. // Initialize NV System
  19. 19. osal_nv_init( NULL ); // 初始化NV(芯片内部FLASH的一块空间)
  20. 20.
  21. 21. // Initialize the MAC
  22. 22. ZMacInit(); // 初始化MAC层(数据链路层)
  23. 23.
  24. 24. // Determine the extended address
  25. 25. zmain_ext_addr(); // 确定芯片的物理地址
  26. 26.
  27. 27.#if defined ZCL_KEY_ESTABLISH
  28. 28. // Initialize the Certicom certificate information.
  29. 29. zmain_cert_init(); // 初始化认证信息
  30. 30.#endif
  31. 31.
  32. 32. // Initialize basic NV items
  33. 33. zgInit(); // 初始化存储在NV中的协议栈全局信息,如网络启动方式等
  34. 34.
  35. 35.#ifndef NONWK
  36. 36.// Since the AF isn't a task, call it's initialization routine
  37. 37. afInit(); // 初始化AF(射频)
  38. 38.#endif
  39. 39.
  40. 40. // Initialize the operating system
  41. 41. osal_init_system(); // 初始化OSAL(操作系统抽象层)
  42. 42.
  43. 43. // Allow interrupts
  44. 44. osal_int_enable( INTS_ALL ); // 使能所有中断
  45. 45.
  46. 46. // Final board initialization
  47. 47. InitBoard( OB_READY ); // 初始化板载IO资源,比如按键
  48. 48.
  49. 49. // Display information about this device
  50. 50. zmain_dev_info(); // 在显示器上显示设备物理地址
  51. 51.
  52. 52. /* Display the device info on the LCD */
  53. 53.#ifdef LCD_SUPPORTED
  54. 54. zmain_lcd_init(); // 在显示器上显示设备信息,比如制造商等
  55. 55.#endif
  56. 56.
  57. 57.
  58. 58.
  59. 59.#ifdef WDT_IN_PM1
  60. 60. /* If WDT is used, this is a good place to enable it. */
  61. 61. WatchDogEnable( WDTIMX ); // 启动看门狗功能
  62. 62.#endif
  63. 63.
  64. 64. /* 进入系统轮询 */
  65. 65. osal_start_system(); // No Return from here
  66. 66.
  67. 67.
  68. 68. return 0; // Shouldn't get here.
  69. 69.} // main()

这个函数中有两个关键的函数调用,代码如下:

  1. //初始化OSAL,包括初始化任务池
  2. osal_init_system();
  3. //轮询任务池
  4. osal_start_system();

可以看到,OSAL的任务调度过程与上节课曾经讲解过的是类似的,也就是初始化任务池和轮询任务池。

osal_init_system()函数和osal_start_system()函数的定义可以在OSAL目录下的OSAL.c文件中找到,OSAL.c所在位置如图所示。
第3章:OSAL 详解 - 图5

osal_init_system()函数代码如下:

  1. uint8 osal_init_system( void )
  2. {
  3. #if !defined USE_ICALL && !defined OSAL_PORT2TIRTOS
  4. // 初始化内存分配系统
  5. osal_mem_init();
  6. #endif /* !defined USE_ICALL && !defined OSAL_PORT2TIRTOS */
  7. // 初始化消息队列
  8. osal_qHead = NULL;
  9. // 初始化OSAL定时器
  10. osalTimerInit();
  11. // 初始化电源管理系统
  12. osal_pwrmgr_init();
  13. #ifdef USE_ICALL
  14. osal_prepare_svc_enroll();
  15. #endif /* USE_ICALL */
  16. // 初始化任务池
  17. osalInitTasks();
  18. #if !defined USE_ICALL && !defined OSAL_PORT2TIRTOS
  19. // Setup efficient search for the first free block of heap.
  20. osal_mem_kick();
  21. #endif /* !defined USE_ICALL && !defined OSAL_PORT2TIRTOS */
  22. #ifdef USE_ICALL
  23. // Initialize variables used to track timing and provide OSAL timer service
  24. osal_last_timestamp = (uint_least32_t) ICall_getTicks();
  25. osal_tickperiod = (uint_least32_t) ICall_getTickPeriod();
  26. osal_max_msecs = (uint_least32_t) ICall_getMaxMSecs();
  27. /* Reduce ceiling considering potential latency */
  28. osal_max_msecs -= 2;
  29. #endif /* USE_ICALL */
  30. return ( SUCCESS );
  31. }

在以上代码中,可以找到找到一个任务池初始化函数osalInitTasks()。顾名思义,它的工作内容就是初始化任务池。

osal_start_system()函数代码如下:

  1. void osal_start_system( void )
  2. {
  3. #ifdef USE_ICALL
  4. /* Kick off timer service in order to allocate resources upfront.
  5. * The first timeout is required to schedule next OSAL timer event
  6. * as well. */
  7. ICall_Errno errno = ICall_setTimer(1, osal_msec_timer_cback,
  8. (void *) osal_msec_timer_seq,
  9. &osal_timerid_msec_timer);
  10. if (errno != ICALL_ERRNO_SUCCESS)
  11. {
  12. ICall_abort();
  13. }
  14. #endif /* USE_ICALL */
  15. #if !defined ( ZBIT ) && !defined ( UBIT )
  16. //主循环
  17. for(;;)
  18. #endif
  19. {
  20. //系统轮询调度
  21. osal_run_system();
  22. #ifdef USE_ICALL
  23. ICall_wait(ICALL_TIMEOUT_FOREVER);
  24. #endif /* USE_ICALL */
  25. }
  26. }

在osal_start_system()函数的主循环中,循环调用了 osal_run_system()函数,该函数主要工作轮询任务池。osal_run_system()函数的定义OSAL.c文件中,代码如下:

  1. 1.void osal_run_system( void )
  2. 2.{
  3. 3. uint8 idx = 0;
  4. 4.
  5. 5. /* 更新时间,并整理出到期的任务。系统的时钟周期是:320us */
  6. 6. osalTimeUpdate();
  7. 7. Hal_ProcessPoll();// 硬件适配层中断查询
  8. 8.
  9. 9. do {
  10. 10. if (tasksEvents[idx])// 查看是否有任务需要处理
  11. 11. {
  12. 12. break;
  13. 13. }
  14. 14. } while (++idx < tasksCnt);// 轮询整个任务池
  15. 15.
  16. 16. if (idx < tasksCnt)//循环结束后,如果idx < tasksCnt表示任务池有任务需要处理
  17. 17. {
  18. 18. uint16 events;
  19. 19. halIntState_t intState;
  20. 20. HAL_ENTER_CRITICAL_SECTION(intState);//关闭中断
  21. 21. events = tasksEvents[idx];//evets中保存了该任务中的待处理事件
  22. 22. tasksEvents[idx] = 0;//清空此任务中的所有待处理事件
  23. 23. HAL_EXIT_CRITICAL_SECTION(intState);//恢复中断
  24. 24.
  25. 25. activeTaskID = idx;
  26. 26. events = (tasksArr[idx])( idx, events ); // 处理任务中的事件
  27. 27. activeTaskID = TASK_NO_TASK;
  28. 28.
  29. 29. HAL_ENTER_CRITICAL_SECTION(intState);//关闭中断
  30. 30. tasksEvents[idx] |= events;//保存还没被处理的事件到任务中
  31. 31. HAL_EXIT_CRITICAL_SECTION(intState);//恢复中断
  32. 32. }
  33. 33.#if defined( POWER_SAVING ) && !defined(USE_ICALL)
  34. 34. else// Complete pass through all task events with no activity? {
  35. 35. osal_pwrmgr_powerconserve(); //如果没有任务需要处理则进入低功耗
  36. 36. }
  37. 37.#endif
  38. 38.
  39. 39. /* Yield in case cooperative scheduling is being used. */
  40. 40.#if defined (configUSE_PREEMPTION)&&(configUSE_PREEMPTION == 0) {
  41. 41. osal_task_yield();
  42. 42. }
  43. 43.#endif

为了更好地体现该函数的轮询逻辑,已对原代码进行了简化。

在上述代码中,重点讲解一下其中的这个do-while循环,代码如下:

  1. 9. do {
  2. 10. if (tasksEvents[idx])// 查看是否有任务需要处理
  3. 11. {
  4. 12. break;
  5. 13. }
  6. 14. } while (++idx < tasksCnt);// 轮询整个任务池

这个循环的主要作用是轮询整个任务池,也就是看一下有没有要处理的任务。循环中只有一个条件判断,如果条件成立,那么就结束循环。

其中的tasksEvents是一个uint16类型的数组,其中的每一个元素都表示一种类型的任务,也就是说,tasksEvents就是一个任务池,tasksCnt是这个任务池的大小。

这个循环的运行逻辑是:

  • 首先,idx的初始值为0;
  • 当 tasksEvents[idx] 的值为0时,表示该任务中没有事情要处理,这时候条件判断不成立,进入下一次循环;
  • 每执行1次循环前,idx加1,然后判断是否小于tasksCnt;
  • 当 tasksEvents[idx] 的值不等于0时,表示该任务中有事情要处理,这时候条件判断成立,于是通过break结束循环;
  • 当循环结束后,如果整个任务池中都没有任务要处理,那么idx必定会 >=tasksCnt。因此,如果 idx < tasksCnt,表示现在任务池中有任务需要处理,并且 tasksEvents[idx] 就是当前需要处理的任务。因此在循环结束后,Z-Stack 先用 if (idx < tasksCnt) 语句来判断有没有任务需要处理。

    任务与事件

    每个任务中可能包含一系列待处理的事情,这些待处理的事情,可以通俗的称为“事件”,例如一个任务中可以包含打开LED灯、关闭窗户和打开空调这3个事件(待处理的事情)。

tasksEvents中的每个元素都是一个uint16类型的变量,每一个元素都表示了一个任务,并且储存了这个任务中包含的一系列事件。那么一个uint16类型的变量是如何储存一系列的事件的呢?笔者将在后续章节详细讲解。

3.2 任务初池始化与事件处理

层次划分

通过前面的章节可以了解到,Z-Stack可以被分成多个层次,例如:

  • MAC层
  • NWK(网络层)
  • HAL(硬件适配层)
  • APP(应用层)

    任务池初始化

    每一个层次都有一个对应的任务来处理本层次的事务,例如MAC层对应一个MAC层的任务、网络层对应一个网络层的任务、HAL对应一个HAL的任务,以及应用层对应一个应用层的任务等,这些各个层次的任务构成一个任务池,这个任务池也就是上节课讲到的tasksEvents数组,如图所示。
    第3章:OSAL 详解 - 图6

上节课曾讲到的任务池初始化函数osalInitTasks()可以在App目录下的OSAL_SampleSw.c文件中找到,如图所示。
第3章:OSAL 详解 - 图7

osalInitTasks()函数定义代码如下:

  1. /*********************************************************************
  2. * @fn osalInitTasks
  3. *
  4. * @brief This function invokes the initialization function for each task.
  5. *
  6. * @param void
  7. *
  8. * @return none
  9. */
  10. void osalInitTasks( void )
  11. {
  12. uint8 taskID = 0;
  13. tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
  14. osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
  15. macTaskInit( taskID++ );
  16. nwk_init( taskID++ );
  17. #if !defined (DISABLE_GREENPOWER_BASIC_PROXY) && (ZG_BUILD_RTR_TYPE)
  18. gp_Init( taskID++ );
  19. #endif
  20. Hal_Init( taskID++ );
  21. #if defined( MT_TASK )
  22. MT_TaskInit( taskID++ );
  23. #endif
  24. APS_Init( taskID++ );
  25. #if defined ( ZIGBEE_FRAGMENTATION )
  26. APSF_Init( taskID++ );
  27. #endif
  28. ZDApp_Init( taskID++ );
  29. #if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
  30. ZDNwkMgr_Init( taskID++ );
  31. #endif
  32. // Added to include TouchLink functionality
  33. #if defined ( INTER_PAN )
  34. StubAPS_Init( taskID++ );
  35. #endif
  36. // Added to include TouchLink initiator functionality
  37. #if defined( BDB_TL_INITIATOR )
  38. touchLinkInitiator_Init( taskID++ );
  39. #endif
  40. // Added to include TouchLink target functionality
  41. #if defined ( BDB_TL_TARGET )
  42. touchLinkTarget_Init( taskID++ );
  43. #endif
  44. zcl_Init( taskID++ );
  45. bdb_Init( taskID++ );
  46. zclSampleSw_Init( taskID++ );
  47. #if (defined OTA_CLIENT) && (OTA_CLIENT == TRUE)
  48. zclOTA_Init( taskID );
  49. #endif
  50. }

这个函数的代码较为复杂,读者暂时按照笔者的讲解思路简单地了解一下其中的原理就可以了。

这个函数首先申请了一个任务池存储空间,也就是这个tasksEvents数组。接着调用了很多带有“init”字样的函数,这些函数的作用是初始化各个层次的任务,例如:

  • 调用macTaskInit()函数初始化MAC层的任务。
  • 调用nwk_init()函数初始化网络层的任务。
  • 调用zclSampleSw_Init()函数初始化应用层的任务。

这个过程跟《OSAL的任务调度原理》章节中创建和初始化任务池是类似的,它们的主要差别在于:

  • 首先,任务池存储的数据结构不同,这里的tasksEvents是一个uint16类型的数组。
  • 其次,tasksEvents支持存放多种类型的任务,例如MAC层的任务、网络层的任务和应用层的任务等。
  • 最后,这里的每一个任务中都可能会包含多个待处理事件。

    事件处理函数

    OSAL_SampleSw.c文件中还定义了一个数组,代码如下:
  1. //创建一个元素类型为pTaskEventHandlerFn的数组
  2. 1.const pTaskEventHandlerFn tasksArr[] = {
  3. 2. macEventLoop, //第1个数组元素
  4. 3. nwk_event_loop, //第2个数组元素
  5. //
  6. //第3个数组元素
  7. 4.#if !defined (DISABLE_GREENPOWER_BASIC_PROXY) && (ZG_BUILD_RTR_TYPE)
  8. 5. gp_event_loop,
  9. 6.#endif
  10. //
  11. //第4个数组元素
  12. 7. Hal_ProcessEvent,
  13. //第5个数组元素
  14. 8.#if defined( MT_TASK )
  15. 9. MT_ProcessEvent,
  16. 10.#endif
  17. //
  18. //第6个数组元素
  19. 11. APS_event_loop,
  20. //
  21. //第7个数组元素
  22. 12.#if defined ( ZIGBEE_FRAGMENTATION )
  23. 13. APSF_ProcessEvent,
  24. 14.#endif
  25. //
  26. //第8个数组元素
  27. 15. ZDApp_event_loop,
  28. //
  29. //第9个数组元素
  30. 16.#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
  31. 17. ZDNwkMgr_event_loop,
  32. 18.#endif
  33. //
  34. //第10个数组元素
  35. 19. //Added to include TouchLink functionality
  36. 20. #if defined ( INTER_PAN )
  37. 21. StubAPS_ProcessEvent,
  38. 22. #endif
  39. //
  40. //第11个数组元素
  41. 23. // Added to include TouchLink initiator functionality
  42. 24. #if defined ( BDB_TL_INITIATOR )
  43. 25. touchLinkInitiator_event_loop,
  44. 26. #endif
  45. //
  46. //第12个数组元素
  47. 27. // Added to include TouchLink target functionality
  48. 28. #if defined ( BDB_TL_TARGET )
  49. 29. touchLinkTarget_event_loop,
  50. 30. #endif
  51. //
  52. 31. zcl_event_loop, //第13个数组元素
  53. 32. bdb_event_loop, //第14个数组元素
  54. 33. zclSampleSw_event_loop, //第15个数组元素
  55. //
  56. //第16个数组元素
  57. 34.#if (defined OTA_CLIENT) && (OTA_CLIENT == TRUE)
  58. 35. zclOTA_event_loop
  59. 36.#endif
  60. 37.};

前面章节中曾经讲到,tasksEvents中的每个任务可以包含0个或者多个待处理的事件,而这个数组类型变量pTaskEventHandlerFn是一个函数指针类型变量,用于指向事件对应的处理函数,因此这段代码定义了一个事件处理函数数组,这个数组中的每一个元素均表示某一个层次任务的事件处理函数,例如:

  • MAC层任务对应的事件处理函数是macEventLoop(),它专门处理MAC层任务中的事件。
  • 网络层任务对应的事件处理函数是nwk_event_loop(),它专门处理网络层任务中的事件。
  • 应用层任务对应的事件处理函数是zclSampleSw_event_loop(),它专门处理应用层任中的事件。

以应用层事件处理函数zclSampleSw_event_loop()为例,可以在zcl_samplesw.h文件中找到该函数的声明,代码如下:

  1. /*
  2. * Event Process for the task
  3. */
  4. extern UINT16 zclSampleSw_event_loop( byte task_id, UINT16 events );

3.3 Z-Stack 事件的应用

本节课的内容是后续课程的基础,希望大家能够好好学习,为后续课程打下良好的基础。

事件的类型与编码

读者可以发现每个层次的事件处理函数的参数都包含1个task id和1个events参数,例如:

  • MAC层事件处理函数,如图所示。
    第3章:OSAL 详解 - 图8

  • 网络层事件处理函数,如图所示。
    第3章:OSAL 详解 - 图9

  • 应用层事件处理函数,如图所示。
    第3章:OSAL 详解 - 图10

以应用层事件处理函数为例,它的第2个参数UINT16 events表示了一个事件集合,其中包含了0个或多个待处理的事件。然而,events是一个16位的变量,它是怎么样表示一个事件集合的呢?

答案是Z-Stack 3.0采用了独热码(one-hot code)的方式对事件类型进行编码。

events的分类
在讲解独热码之前,先来了解一下events的分类。

  • events的最高位为1时,表示这是系统事件集合,即events中的事件全是系统事件。
  • events的最高位为0时,表示这是用户事件集合,即events中的事件全是用户事件。

用户事件可以由开发者自行定义其含义,以及相应的处理。

使用独热码
采用独热码的方式,把所有的用户事件编码列举出来,并从中分析独热码的规律,见下表。

2进制编码 16进制编码 事件名称
0000 0000 0000 0001 0x00 01 用户事件A
0000 0000 0000 0010 0x00 02 用户事件B
0000 0000 0000 0100 0x00 04 用户事件C
0000 0000 0000 1000 0x00 08 用户事件D
0000 0000 0001 0000 0x00 10 用户事件E
0000 0000 0010 0000 0x00 20 用户事件F
0000 0000 0100 0000 0x00 40 用户事件G
0000 0000 1000 0000 0x00 80 用户事件H
0000 0001 0000 0000 0x01 00 用户事件I
0000 0010 0000 0000 0x02 00 用户事件J
0000 0100 0000 0000 0x04 00 用户事件K
0000 1000 0000 0000 0x08 00 用户事件L
0001 0000 0000 0000 0x10 00 用户事件M
0010 0000 0000 0000 0x20 00 用户事件N
0100 0000 0000 0000 0x40 00 用户事件O

其中事件名称可以根据实际需命名,例如开灯事件、关灯事件或者发送警告事件等。

从这些编码中,可以得出2个规律:

  1. 除了用于表示系统事件或者用户事件的最高位,其他15个比特位中,只有1位为1,其他位均为0。
  2. 使用15个比特位表示15种用户事件。

    这两个规律也是独热码的规律。

利用规律1,可以很容易地理解为什么events可以表示一个事件集合。现在假设events的值为0000 0000 0101 0101,其中的右起第1、3、5和7位为1,于是可以理解为事件集合events包含了用户事件A、C、E和G。

利用规律2,可以得到events最多可以包含15种用户事件。

定义用户事件

可以使用以下方法在zcl_samplesw.h文件中定义一个用户事件。
1.定义事件名称和对应的编码。

  1. #define SAMPLEAPP_TEST_EVT 0x0040

2.把它复制到zcl_samplesw.h文件中的如图所示位置。
第3章:OSAL 详解 - 图11

处理用户事件

可以在zcl_samplesw.c文件中的应用层事件处理函数中添加相关的处理,代码如下:

  1. uint16 zclSampleSw_event_loop( uint8 task_id, uint16 events )
  2. {
  3. afIncomingMSGPacket_t *MSGpkt;
  4. (void)task_id; // Intentionally unreferenced parameter
  5. //用户事件:SAMPLESW_TOGGLE_TEST_EVT
  6. if( events & SAMPLESW_TOGGLE_TEST_EVT )
  7. {
  8. osal_start_timerEx(zclSampleSw_TaskID,SAMPLESW_TOGGLE_TEST_EVT,500);
  9. zclGeneral_SendOnOff_CmdToggle( SAMPLESW_ENDPOINT, &zclSampleSw_DstAddr, FALSE, 0 );
  10. //消除已经处理的事件然后返回未处理的事件
  11. return (events ^ SAMPLESW_TOGGLE_TEST_EVT);
  12. }
  13. //SYS_EVENT_MSG:0x8000表示系统事件,也就是说检测uint16最高位
  14. if ( events & SYS_EVENT_MSG )
  15. {
  16. //省略系统事件的处理代码
  17. .........
  18. // 消除系统事件标识然后返回未处理的事件
  19. return (events ^ SYS_EVENT_MSG);
  20. }
  21. #if ZG_BUILD_ENDDEVICE_TYPE
  22. //用户事件:SAMPLEAPP_END_DEVICE_REJOIN_EVT
  23. if ( events & SAMPLEAPP_END_DEVICE_REJOIN_EVT )
  24. {
  25. bdb_ZedAttemptRecoverNwk();
  26. return ( events ^ SAMPLEAPP_END_DEVICE_REJOIN_EVT );
  27. }
  28. #endif
  29. //用户事件:SAMPLEAPP_LCD_AUTO_UPDATE_EVT
  30. if ( events & SAMPLEAPP_LCD_AUTO_UPDATE_EVT )
  31. {
  32. UI_UpdateLcd();
  33. return ( events ^ SAMPLEAPP_LCD_AUTO_UPDATE_EVT );
  34. }
  35. //用户事件:SAMPLEAPP_KEY_AUTO_REPEAT_EVT
  36. if ( events & SAMPLEAPP_KEY_AUTO_REPEAT_EVT )
  37. {
  38. UI_MainStateMachine(UI_KEY_AUTO_PRESSED);
  39. return ( events ^ SAMPLEAPP_KEY_AUTO_REPEAT_EVT );
  40. }
  41. // 处理刚才自己定义的用户事件:SAMPLEAPP_TEST_EVT
  42. if ( events & SAMPLEAPP_TEST_EVT )
  43. {
  44. printf("Hello World!\r\n");
  45. //消除已经处理的事件然后返回未处理的事件
  46. return ( events ^ SAMPLEAPP_TEST_EVT );
  47. }
  48. // Discard unknown events
  49. return 0;
  50. }

通过前面讲解可以了解到,每一种用户事件类型编码中只有1位为1,其他比特位为0。SAMPLEAPP_TEST_EVT的事件类型编码为0x0040,其二进制数为:0000 0000 0100 0000。这个编码的右起第7为1,其余位为0。

于是上述代码利用events & SAMPLEAPP_TEST_EVT让事件集合参数events与预定义的事件类型SAMPLEAPP_TEST_EVT做与运算,判断events中的右起第3位是否为1。如果为1,那么events & SAMPLEAPP_TEST_EVT的值为1,这表示事件集合参数events包含SAMPLEAPP_TEST_EVT这个事件,因此程序执行对应的处理代码,即执行:

  1. printf("Hello World!\r\n");

接着,代码中利用events ^ SAMPLEAPP_TEST_EVT把events中的第3位清0,然后把这个值作为函数的返回值,表示events中的这个事件已经被处理了。

这段代码中用到了 & 和 ^ 运算,如果不了解这两种运算,需要先补习一下。

触发用户事件

前面已经定义好事件类型和对应的处理方式了,但是需要在OSAL中触发该事件后,OSAL才会执行对应的处理代码。

OSAL提供了专门的API来触发事件。展开OSAL层,可以找到OSAL_Timers.h文件,如图所示。
第3章:OSAL 详解 - 图12

在OSAL_Timers.h文件中,可以找到触发事件的API,函数声明如下:

  1. uint8 osal_start_timerEx(uint8 task_id,uint16 event_id,uint32 timeout_value);

该函数有三个参数,其说明如下:

  • task_id:任务ID,用于标记这个事件是属于哪一个层次的任务。
  • event_id:事件ID,用于标记这个事件的类型。
  • timeout_value:表示多少毫秒后才处理这个事件。

如果希望在触发事件的3s后处理刚才自定义的事件,可在应用层初始化函数zclSampleSw_Init()的末尾位置添加如下代码:

  1. osal_start_timerEx(
  2. zclSampleSw_TaskID,//标记本事件属于应用层任务
  3. SAMPLEAPP_TEST_EVT,//标记本事件的类型
  4. 3000);//表示3000ms后才处理这个事件

其中,zclSampleSw_TaskID是一个全局变量,用于标记这个事件是属于应用层任务的。

添加代码后的效果如图所示。
第3章:OSAL 详解 - 图13

调试仿真

右击工程名称,选择Options,如图所示。
第3章:OSAL 详解 - 图14

选择Debugger,然后在Driver选项卡中选择Texas Instruments,最后单击OK按钮完成设置,如图所示。
第3章:OSAL 详解 - 图15

使用仿真器把配套的ZigBee开发板连接到电脑。

单击Download and Debug按钮进行程序的编译、链接和下载并进入仿真模式,如图所示。
第3章:OSAL 详解 - 图16

进入仿真模式之后,单击Go按钮运行程序,如图所示。
第3章:OSAL 详解 - 图17

程序在运行3后秒会在Terminal I/O窗口中输出“Hello World!”,如图所示。

第3章:OSAL 详解 - 图18

3.4 使用动态内存

动态内存简介

在编写程序时,开发者可能不能确定需要使用多少内存来保存数据,希望在程序运行的过程中根据实际需要来使用相应的内存空间。这种能够在程序运行的过程中申请和释放的内存空间称为动态内存

Z-Stack 3.0 中动态内存分配的API在OSAL_Memory.h文件中,这个文件在OSAL目录下,如图所示。
第3章:OSAL 详解 - 图19

申请与释放动态内存

在OSAL_Memory.h文件中,可以找到两个重要的API,定义如下:

  1. /**
  2. * @fn osal_mem_alloc
  3. *
  4. * @brief 动态申请内存空间
  5. *
  6. * @param size - 申请多少个字节的内存空间
  7. *
  8. * @return 返回该内存空间的指针
  9. */
  10. void *osal_mem_alloc( uint16 size );
  11. /**
  12. * @fn osal_mem_free
  13. *
  14. * @brief 动态释放内存空间
  15. *
  16. * @param ptr - 待释放的内存空间指针
  17. *
  18. */
  19. void osal_mem_free( void *ptr );

如果内存空间不用了,一定要调用这个API来释放内存空间。

动态内存的操作

在申请完动态内存后,可以调用内存操作API来使用这些内存。内存操作API在OSAL.h文件中,下面这两个API是比较常用的,定义如下:

  1. /**
  2. * @fn osal_memcpy
  3. *
  4. * @brief 把内存空间的内容复制到另一个内存空间中
  5. *
  6. * @param void* - 目标内存空间
  7. * @param const void GENERIC * - 源内存空间
  8. * @param unsigned int - 复制多少个字节
  9. *
  10. * @return
  11. */
  12. void *osal_memcpy(void*, const void GENERIC *,unsigned int);
  13. /**
  14. * @fn osal_memset
  15. *
  16. * @brief 把内存空间的值设置为指定的值
  17. *
  18. * @param dest - 内存空间
  19. * @param value - 指定的值
  20. * @param len - 把从dest起的len个字节的存储空间的值设置为value
  21. *
  22. * @return
  23. */
  24. extern void *osal_memset( void *dest, uint8 value, int len );

使用动态内存

在上节课中的事件处理函数中做一些修改,修改后代码如下:

  1. //事件:SAMPLEAPP_TEST_EVT
  2. if ( events & SAMPLEAPP_TEST_EVT )
  3. {
  4. //字符串:”Hello World!\n”
  5. char *str = "Hello World!\n";
  6. //从堆空间中申请32个字节的内存空间
  7. char *mem = osal_mem_alloc(32);
  8. //如果申请成功
  9. if (mem != NULL) {
  10. //清零内存空间
  11. osal_memset(mem, 0, 32);
  12. //将字符串拷贝到内存空间中
  13. osal_memcpy(mem, str, osal_strlen(str));
  14. //打印内存空间内存
  15. printf(mem);
  16. //释放内存空间
  17. osal_mem_free(mem);
  18. }
  19. //重新触发事件,3000毫秒后执行
  20. osal_start_timerEx(zclSampleSw_TaskID, SAMPLEAPP_TEST_EVT, 3000);
  21. //消除已经处理的事件,然后返回未处理的事件
  22. return ( events ^ SAMPLEAPP_TEST_EVT );
  23. }

这段代码中,在执行完事件处理之后,重新触发了该事件,实现了以恒定的时间间隔不断触发该事件。

调试仿真

使用仿真器把配套的ZigBee开发板连接到电脑。

单击Download and Debug按钮进行程序的编译、链接和下载并进入仿真模式,如图所示。
第3章:OSAL 详解 - 图20

进入仿真模式之后,单击Go按钮运行程序,如图所示。
第3章:OSAL 详解 - 图21
程序运行后,会每隔3秒在Terminal I/O窗口中输出“Hello World!”,如图所示。
第3章:OSAL 详解 - 图22