【Zstack】快速掌握简单应用层开发

基于TI样例快速开发Zstack协议栈应用层

 本文基于TI GenericApp样例 梳理样例应用层函数,学习协议栈和相关编程思路。

知识储备

  • C语言基础(包括回调函数、静态变量、指针、结构体以及一些很基础的概念)
  • 初步程序设计思想(过程设计,模块化设计,代码复用,自顶向下)
  • Zstack,OSAL的基本运作机制(轮询机制等概念,有个流程图)
  • 如果对后面文章哪一处有疑问,建议问问神奇的海螺

本文关注要点

  • 协议栈与单片机裸机编程,在编程上的区别
  • 应用层的应用是如何在协议栈和osal中工作的
  • GenericApp中,TI样例函数之间的关系
  • C语言编程通用技巧

协议栈与单片机裸机编程(有初步程序设计概念的可以直接跳过)

  平时无论是最原始的51亦或是其他单片机,学习之初是 了解硬件底层寄存器+在IDE中直接操作相关寄存器 来完成基础功能,比如点个灯,点个流水灯,LCD俄罗斯方块等。基本流程就是自己定一个主函数,利用一些编程技巧就可以实现一定的功能。而Zstack协议栈,则是一下跨越到了系统层级的编程,跨度很大,如果不找到一个合理的切入口去解构协议栈,那建议放弃治疗。

  说到切入口,根据协议的层级区分(zigbee网络结构),我们能够轻易介入的肯定是最上层的应用层。像Zstack一样的成熟协议栈,会做两件事方便使用者快速开发应用层

  •   提供底层封装好的,多样的函数和宏定义(比如串口读写函数、按键函数等)。这里体现到一个模块化设计和代码复用的思想,以往裸机编程反复编写的针对寄存器和其余底层配置的操作和一系列操作的组合功能,被封装成宏参数和函数,分布在底层C文件中等待调用。
      这样做一来 方便阅读和编写,二来 提高代码复用 的效率(划重点!!)。在应用层看来,就直接变成了,调用多个功能函数+逻辑来进一步组合完成目标,同时也无需关心底层的实现,无线的传输等功能了。其实不仅是这个协议栈,这样逐步模块化,积累的过程,在任何上规模的程序中都是必须的。(STM32 之所以方便,就是因为公司提供了完备的功能函数)

  •   提供基础样例,方便用户极快的了解应用层相关函数的使用,同时也作为开发模板。在GenericApp中,基础的功能是两个模块完成组网绑定,互传字符串hello world并显示在LCD上。

  由此可见,编程观念要转换,从事无巨细的底层功能实现中解放出来,善于利用现成函数,完成更大规模的逻辑。关注点从操作寄存器—>利用函数(学过STM32或者熟悉编程的很容易迁移概念)。同样,在利用底层函数的同时,我们也可以进一步利用编程技巧和思想,封装我们自己编写的功能,松耦合紧内聚,提高复用率。

  针对于Zstack来说,要初步改造或者添加内容,就必须了解添加改造部分与原有功能之间的联系。以添加应用为例,需要了解应用如何加入osal进行工作,如何调用底层来实现应用。样例给出了规范的高可扩展性的写法。

OSAL基础工作流程(以应用层为核心主线,其余流程作用请善用搜索引擎,嫌繁琐可以后跳到最后流程总结)

  下图为Zstack 2.5.1为例子介绍工程文件内容(其余版本流程并无差别)

  • 接下来选取的函数都是与创建应用紧密相关的函数,或者是直接需要改动的,或者是跟流程相关的。
  • 重点关注目录ZMain 和App
  • OSAL系统,从ZMain目录下的ZMain.c 文件开始执行

    /*********************************************************************
    * @fn      main
    * @brief   First function called after startup.
    * @return  don't care
    */
    int main( void )
    {
        ...
    
    // Initialize the operating system
    osal_init_system();
    
        ...
    osal_start_system(); // No Return from here
    
    return 0;  // Shouldn't get here.
    } // main()
    
    • 这里截取了和应用层相关的两个函数,一个是osal初始化的函数 osal_init_system(),另一个是系统正常运作函数,start函数就是日常死循环,里面有详细的轮询代码。
      根据OSAL的工作流程,初始化是分配注册ID顺序和初始化应用或者系统其他任务,字面意思。
      初始化的重要性不言而喻,类似应用中需要调用的IO或者寄存器配置初始化全在里面
  • 进入 osal_init_system() 瞄一眼,看到init_task函数(需要用户改动)这个inittask函数很重要!!(敲黑板)

    /*********************************************************************
    * @fn      osal_init_system
    *
    * @brief
    *
    *   This function initializes the "task" system by creating the
    *   tasks defined in the task table (OSAL_Tasks.h).
    *
    * @param   void
    *
    * @return  SUCCESS
    */
    uint8 osal_init_system( void )
    {   
        ...
        // Initialize the system tasks.
        osalInitTasks();
        ...
        return ( SUCCESS );
    }  
  • 为什么说这个task函数重要的很?请看!(函数位置在样例的App目录OSAL_GenericApp.c)

    /*********************************************************************
    * @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++ );
        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
        GenericApp_Init( taskID );//前面的所有任务都是默认不需要用户改动的,这一条才是用户添加的自定任务,有多少就在后面继续++一个一个初始化
    }        
    
    • 从代码结构上来看,这个就是单纯靠taskID这个变量不断++来 确定各个任务被轮询的顺序(优先级),回看OSAL的运行机制,这里的流程用代码很清晰的展现了出来。
    • init里添加任务后,在上方 const 定义的tasksArr函数域里 按顺序 加上任务在被轮询时 具体执行功能的函数 命名习惯是***_ProcessEvent。有关系统初始部分,到此结束。
    • 这个初始化可以和别的C文件打包在一起,分开来单独放一页只是为了结构简明。
  • 打开样例,TI已经帮我们设计好了一个模板(大法好),这个模板就如同他的名字一样,很通用,也是我们编程最重要的一环。

    • 除去前面必要的头文件引入、结构体、变量、函数声明,前面提过的 init函数 就在第一个,ProcessEvent函数 在第二个,整个C文件,整个应用也就是围绕这两个函数在写。
    • init内容就是按部就班的初始化无线电参数、IO、中断、串口、等,其中,如果你对于zigbee网络没有其他要求的话(改用组播或者其他组网方式),可以直接使用默认的绑定方式,初始化条件已经写好。其余个性化初始,就自己写。建议使用内部集成的函数,比如关于硬件部分的延时在HAL目录下,串口的HAL和MT都有,其中MT的串口是对HAL串口设置的进一步封装(可以多任务多串口安排优先级等高级操作),系统相关函数就在OSAL目录下找,比如 osal_memcpy 等常用操作。
    • 举个例子,在样例init基础上直接添加如下代码,进行串口的初始化(HAL串口),位置建议在#if这类定义的前面,其中这个 halUARTCfg_t 是定义在haluart头文件的结构体。
    halUARTCfg_t uartConfig;
    //init uart
    uartConfig.configured           = TRUE;              // 2x30 don't care - see uart driver.
    uartConfig.baudRate             = HAL_UART_BR_115200;
    uartConfig.flowControl          = FALSE;
    uartConfig.flowControlThreshold = 64;   // 2x30 don't care - see uart driver.
    uartConfig.rx.maxBufSize        = 128;  // 2x30 don't care - see uart driver.
    uartConfig.tx.maxBufSize        = 128;  // 2x30 don't care - see uart driver.
    uartConfig.idleTimeout          = 6;    // 2x30 don't care - see uart driver.
    uartConfig.intEnable            = TRUE; // 2x30 don't care - see uart driver.
    uartConfig.callBackFunc         = rxCB; //重点配置,rxCB是回调函数
    HalUARTOpen (0, &uartConfig); 
    
    • 接下来会详细分析ProcessEvents 函数,这个函数是整个应用任务的核心,它决定了当轮询到应用头上并发生了些什么的时候,所做的操作内容。这里TI的这个函数给的结构非常清楚。很值得参考其写法。其中用了很多看起来很麻烦的变量,其实都和真正的编写程序不相关。
    • 这个函数大体意思是,用if判断事件events有无发生,进而使用switch判断事件类型,用switch来对每一种事件有一个反馈
    • 我们在这个阶段只需关注其中给定的
      • 按键事件 KEY_CHANGE
      • 无线接收事件 AF_INCOMING_MSG_CMD
      • 无线发送事件(通常由 AF_DataRequest 函数来触发),其中无线发送可以由 AF函数所在的任意函数来触发。
    • 我们需要按键发生什么就到case下 GenericApp_HandleKeys 函数中去填写相关逻辑,同理,无线接收到数据要做处理,就去 GenericApp_MessageMSGCB 中搞点事情。
    • 无线发送大体可分定时发送,突发事件发送(事件回调),某些特殊动作发送,如果要在运行中定时发送 就在if GENERICAPP_SEND_MSG_EVT 处进行设置,突发和特殊动作,就在相应的动作函数中添加AF发送函数,在函数中填写相关信息,就可实现发送无线消息。
    • 如果想自己添加其他不需要switch 和if 判断事件类型也要执行的动作。。。。。直接写在函数体里面就好啦(‘・ω・’)
      /*********************************************************************
       * @fn      GenericApp_ProcessEvent
       *
       * @brief   Generic Application Task event processor.  This function
       *          is called to process all events for the task.  Events
       *          include timers, messages and any other user defined events.
       *
       * @param   task_id  - The OSAL assigned task ID.
       * @param   events - events to process.  This is a bit map and can
       *                   contain more than one event.
       *
       * @return  none
       */
      uint16 GenericApp_ProcessEvent( uint8 task_id, uint16 events )
      {
              ...
      
        if ( events & SYS_EVENT_MSG )
        {
          MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( GenericApp_TaskID );
          while ( MSGpkt )
          {
            switch ( MSGpkt->hdr.event )
            {
              ...
      
              case KEY_CHANGE:
                GenericApp_HandleKeys( ((keyChange_t *)MSGpkt)->state, ((keyChange_t *)MSGpkt)->keys );
                break;
      
              ....    
      
              case AF_INCOMING_MSG_CMD:
              GenericApp_MessageMSGCB( MSGpkt );
              break;
              ...
      
              default:
              break;
          }
              ...
          }
      
          // return unprocessed events
          return (events ^ SYS_EVENT_MSG);
      }
      
      // Send a message out - This event is generated by a timer
      //  (setup in GenericApp_Init()).
      if ( events & GENERICAPP_SEND_MSG_EVT )
      {
          // Send "the" message
          GenericApp_SendTheMessage();
      
          // Setup to send message again
          osal_start_timerEx( GenericApp_TaskID,
                              GENERICAPP_SEND_MSG_EVT,
                              GENERICAPP_SEND_MSG_TIMEOUT );
      
          // return unprocessed events
          return (events ^ GENERICAPP_SEND_MSG_EVT);
      }
      ...
      
      // Discard unknown events
      return 0;
      }
      
  • 流程总结下来异常简单(OSAL是一个简单的实时操作系统):主函数入口int main() –> osal_init_system() 处初始化用户自定义任务,其中的 osalInitTasks() 需要用户安排的明明白白。–> osal_start_system() 开始轮询死循环,轮询到我们的应用了,执行的操作和反馈内容需要我们自己写(大佬不按模板来也可以)。
  • 整体是很简单的一根筋–>死循环模型。

自顶向下设计流程

  • 这是一个从需求出发的设计思路。目的是把握应用的大体功能和编程实践的具体步骤顺序。
  • 思考的第一步是提出利用zigbee网络来达成的某种目的
  • 接下去思考实现目的所要使用的功能(无线收发、串口通讯、按键等)
  • 安排各种功能的逻辑交互
  • 简单的,可以根据模板和需求功能开始具体编程,来实现相应简单功能。

自底向上丰富功能

  • 在具体编程阶段,需要多种多样的功能函数来满足奇奇怪怪的需求和巧妙的技巧
  • 了解更多的关于协议栈的功能函数有利于快速编程,快速实现功能,节约代码量,进一步熟练需要实践经验
  • 简单应用部分常用HAL、MT、OSAL目录中规定的函数和C语言特色功能函数,数量不少,需要熟练掌握搜索引擎
  • 这部分自底向上是专精于zigbee和特定芯片的过程

你可能感兴趣的:(zigbee,初学,zigbee,Zstack,初学)