查看: 48923|回复: 55

Z-STACK 2006协议栈硬件驱动的精简

[复制链接]
kennan 发表于 2010-11-28 10:34:10 | 显示全部楼层 |阅读模式
Z-STACK 2006协议栈硬件驱动的精简

By  kennan
(2010.11)
TI的zigbee协议栈Z-Stack因为考虑他公司自己软件维护以及多种类型处理器的兼容性问题,协议栈中关于硬件驱动部分写的非常晦涩难懂,很多新手往往在这些一层一层的#define中被转的晕头转向,还有的人手里的板子没有按钮,简单到只有1~2个led 灯和射频部分,针对TI公司 BB,DB,EB板的那些例子程序都无法在没有按键的板子上直接运行演示(TI的很多例子用joystick按钮来演示的,这个很多板子没有或者不兼容TI)。一直想对现有的协议栈硬件驱动部分进行一下精简,剥掉原有驱动对硬件寄存器的包装,让新手更容易在已有单片机基础上来理解和修改应用范例,尽快在手头的板子上运行例程,同时本人有机会自己练练手。
我手里有两块2430的板子,当时花了N多大洋买的,后悔啊。板子上只有射频基本电路,串口,以及接在P0_0和P0_1端口上的两个LED。再无别的按钮啊(对,还有一个复位按钮 ),液晶啊,摇杆啊,反正是统统地没有,相当地裸。本文档修改的目的是用最直接的方式来控制这两个LED端口和串口。其余关于多于的led,按钮之类的统统删除,搞就搞一个最简单的 。
修改以2006版本的1.4.3-1.2.1中的GenericApp为原始例程,以DB板为原型,精简硬件接口驱动部分程序,并将App任务精简到一个协调器建立网络之后定时向网络广播发送数据,其余节点(不管是router还是end device)则只是等待有广播数据接收,然后经串口送PC显示。(修改和读码我还是推荐配合使用source insight,否则。。。。 。)
一   GenericApp.c文件的解释和处理
首先处理GenericApp.c文件。这个文件是实现无线数据收发的实现所在,包括初始化端点描述符、简单描述符、cluster列表这些,也包括应用层面的一些消息处理内容。我们现在要做的是把所有关于key和LED的东西统统干掉,只保留coordinator的定时发送数据和router或end device的接收消息并串口输出数据功能,这样这个例程就不需要key来控制了。
开工吧。
对于coordinator,注释掉以下三个函数声明
//void GenericApp_ProcessZDOMsgs( zdoIncomingMsg_t *inMsg );
//void GenericApp_HandleKeys( byte shift, byte keys );
//void GenericApp_MessageMSGCB( afIncomingMSGPacket_t *pckt );
只留下
void GenericApp_SendTheMessage( void );因为我们只要求coordinato发送数据就可以了。调用这个函数就OK了。
Init函数中的  RegisterForKeys( GenericApp_TaskID );也注释掉,我们没有键盘哦。

以下两个register函数注释掉
  ZDO_RegisterForZDOMsg( GenericApp_TaskID, End_Device_Bind_rsp );
  ZDO_RegisterForZDOMsg( GenericApp_TaskID, Match_Desc_rsp );
这两个是ZDO方面的消息返回到应用层面处理的一个请求注册,注册后就可以把底层的数据活动情况通过消息的方式发到应用层,我们不打算处理绑定问题和匹配问题,最后完成的目标就是广播发送,所以不需要这两个。
在GenericApp_ProcessEvent函数的系统消息“if ( events & SYS_EVENT_MSG )“处理循环中,我们只保留
case ZDO_STATE_CHANGE:
          GenericApp_NwkState = (devStates_t)(MSGpkt->hdr.status);
          if ( (GenericApp_NwkState == DEV_ZB_COORD)
              || (GenericApp_NwkState == DEV_ROUTER)
              || (GenericApp_NwkState == DEV_END_DEVICE) )
          {
            // Start sending "the" message in a regular interval.
            osal_start_timerEx( GenericApp_TaskID,
                                GENERICAPP_SEND_MSG_EVT,
                                GENERICAPP_SEND_MSG_TIMEOUT );
          }
          break;
这一部分,因为我打算让这个coodinator建立网络后就定时广播数据,所以当ZDO_STATE_CHANGE发生后,启动定时器,在定时器的消息处理函数中发送消息。这段程序原意是不管是COORD,DEV_ROUTER还是END_DEVCIE,建立起网络后(或加入网络后)都启动定时器,这和我们的初衷不一样,我们只要求COORD有这个功能,所以把if里面只保留GenericApp_NwkState == DEV_ZB_COORD判断条件,其余两个||的条件删除。
接下来:
  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);
  }
这段程序保留,目的是得到定时消息后通过无线把信息发送出去,然后重新启动定时器,这样COORD就能一直按照定时间隔发数据出去啦,网络中其余节点的工作就是不停地收到广播信息,然后用串口传到PC呗。
文件中剩下后面的函数只保留:
void GenericApp_SendTheMessage( void )
{
  char theMessageData[] = "Hello World";

  if ( AF_DataRequest( &GenericApp_DstAddr, &GenericApp_epDesc,
                       GENERICAPP_CLUSTERID,
                       (byte)osal_strlen( theMessageData ) + 1,
                       (byte *)&theMessageData,
                       &GenericApp_TransID,
                       AF_DISCV_ROUTE, AF_DEFAULT_RADIUS ) == afStatus_SUCCESS )
  {
    // Successfully requested to be sent.
  }
  else
  {
    // Error occurred in request to send.
  }
}
这个是发数据的具体实现函数。其余的注释掉,这样coord的主程序就定型了,里面没有LED,也没有key,如果想加进去LED作指示,等我们处理好LED之后再回到这里在想要的地方加进去就是了。
到这里,coordinator的app主程序就算改好了,router和end device的修改方式类似,不过网络状态变化消息放在哪里就好,什么也不用干。但是要保留
void GenericApp_MessageMSGCB( afIncomingMSGPacket_t *pckt );
去掉:void GenericApp_SendTheMessage( void );具体程序见附件源代码。
        至此,我们这个例子就不需要key和LED了,uart还没有修改,接下来进行下面的工作。


 楼主| kennan 发表于 2010-11-28 10:35:49 | 显示全部楼层
本帖最后由 kennan 于 2010-11-28 19:20 编辑

回复 1# kennan


    二  HAL文件夹中相关文件的解释和处理

要修改硬件驱动部分,自然要先看一下HAL文件夹中的相关内容,并研究一下该文件夹中的文件和整个系统之间的关联程度,否则改完了一编译错误N多啊。
(1)        common文件夹
这个文件夹下有两个文件hal_assert.c和hal_drivers.c。
先看相对简单的hal_drivers.c文件:大致浏览一下这个文件发现,void Hal_ProcessPoll ()这个函数不能删除,这里面有HalTimerTick()和HalUARTPoll()两个函数调用,并且Hal_ProcessPoll()函数在OSAL层的主循环中被调用,用来更新timer计时和串口查询,如果要删除该函数,需要在osal主循环中直接调用HalTimerTick()(这个我认为还是要保留,不能cut了,zstack定时器的使用还是相当好的,可以像VC、VB那样自由地添加timer,timer离不开这个东东的)和HalUARTPoll()两个函数。到这里我相信如果我直接操作串口寄存器,用查询的方式来发数据,用中断后发消息的方式来接收串口数据,那么整个hal_drivers.c文件可以删除了,不过osal那个添加任务初始化及processevent的地方也要相应地去掉这一部分。另外,这个文件里面实现了led 闪烁功能,实际上就是建立一个任务、启动一个定时器来闪烁LED;还有就是HalKeyPoll()函数的调用,这个是用来查询键盘的,我没有键盘,闪烁也不要了;最后就是还有一部分初始化ADC啊,DMA,UART啊这些片内外设的void HalDriverInit (void)函数,如果把这些初始化函数调用换到别处,这个文件也无关了(现在先保留一会儿,等最后处理好target文件夹下drivers里面相应内容再回来料理这个。
再看hal_assert.c文件,assert这件事儿在VC++里面会大量出现,我个人认为在zigbee这样的协议栈可以没有。这个文件看是和系统无关,无非就是一个错误assert的东西,我们可以不要错误assert,直接删除如何?实践证明直接删除存在很多问题,最严重的就是mac层的lib库用到了这里面的一个函数void halAssertHandler(void),那么这个文件无论如何也不可能删除了,因为lib文件我们没办法修改去掉对该文件中函数的调用。那么如果我们真的不想要assert该如何处理?我的方法是新建一个文件hal_assertfake.c,其中只有void halAssertHandler(void)这个函数的空实现。然后把原来的hal_assert.c从项目管理组中删除(更简单的办法是在这个文件上右键options,然后 这个选上复选框,编译的时候就会忽略它,把新建的假文件hal_assertfake.c加到common文件夹,编译一下,顺利骗过 。大家可能会说:“我写程序也没用到这个hal_assert.c,看代码也没见到,搞不搞掉他我无所谓!”其实不然,这个文件中对LED,键盘这些东西都搞搞,结果那边的相关部分就不能删除太干净,相当不爽,所以先把这个干掉。
,我先睡了。
继续。
(2)        include文件夹
这个文件夹是各个硬件单元的.c文件的头,可以考虑改变;初步计划改变key,led,lcd,和uart这几个。其余的多是片内外设操作的定义,留着吧。这里对相应头文件的更改过程在每一个具体.c文件的修改过程中进行。
(3)        target文件夹
这个文件夹里面基本就是board上的硬件驱动部分了。分成DB,EB两种TI的板子,这里我选择DB板子来修改。这下面有config文件夹:里面只有一个hal_board_cfg.h文件有实际内容,是对电路板的一个配置,接下来的工作难免会针对我自己的板子进行裁剪;includes文件夹:里面几个文件都动不了;drivers文件夹,这里面就是硬件驱动的具体实现文件了,我们接下来的大部分工作就要在这些文件夹下做手脚了。
        开工吧。
1)先处理hal_lcd.c,右键options,然后 ,make一下,发现有一个错误,问题是在前面提到的hal_drivers.c文件中有
  /* LCD */
#if (defined HAL_LCD) && (HAL_LCD == TRUE)
  HalLcdInit();
#endif
我们没有LCD,那好,在hal_board_cfg.h文件中有
/* Set to TRUE enable LCD usage, FALSE disable it */
#ifndef HAL_LCD
#define HAL_LCD TRUE
#endif
把TRUE改成FALSE,重新make项目,ok,这样液晶支持就去掉了,愿意把hal_lcd.c从项目文件中删除也可以,不删除就exclude from buid,效果一样。

2)接下来处理hal_key.c,看一下hal_board_cfg.h文件中,有
#ifndef HAL_KEY
#define HAL_KEY TRUE
#endif
把TRUE改成FALSE,然后把hal_key.c ,make项目,会有几处错误,找到几处调用hal_key.c文件中函数的地方,注释掉,或者用条件编译#if (HAL_KEY == TRUE) #endif 来处理掉,(主要是zmain文件中的static ZSEG void zmain_ext_addr( void )函数中有一处调用)这样按键就统统被去掉了。可以把这个文件从项目中删除了。
从hal_board_cfg.h中的配置看来,剩下ADC,DMA,LED,UART,ASE等配置选项目前都保留吧,片内外设我们不在乎,片外的LED,我们要用啊。

3)下面开始搞led,这文件我们要保留,但是做最大精简,做到直接面对硬件,去掉协议栈的一层层包装。 ,周末有点儿饿呢。再搞一会儿去吃饭。
先从hal_led.h文件开始,我们只保留LED1和LED2两个,blink,sleep,ExitSleep,GetState这些
都不打算要了,我只要求能点亮LED或者熄灭LED就好,大刀阔斧开搞,具体会有哪些麻烦我们拭目以待吧。
砍掉不要的,最后头文件剩下内容:
#define HAL_LED_1     0x01
#define HAL_LED_2     0x02
#define HAL_LED_ALL   (HAL_LED_1 | HAL_LED_2)
/* Modes */
#define HAL_LED_MODE_OFF     0x00
#define HAL_LED_MODE_ON      0x01

extern void HalLedInit( void );
extern uint8 HalLedSet( uint8 led, uint8 mode );
这些就够我这个垃圾板子使用了,接下来去hal_led.c文件中搞搞。
Hal_led.c文件:
把上面头文件中保留的两个函数保留,其余统统干掉。Make ,OK。
好,继续,在make一下,发现还是有错误,主要是ZDApp中有调用过LED的事情。进入ZDApp函数,先把关于led 调用的东西注释了,等我们解决LED这部分事情后,想加入LED功能的话,再回头按照新写的led 程序填进去想要的东西。

         ??什么?LED还是想要的?起码我可以让发送数据的时候亮一下灯,表示我的板子还活着啊。那好,我们加两个函数好了。注意:现在我们写led 的驱动就完全按照寄存器操作的方式来了,不用那么多#define包裹起来。
我们现在来为hal_led.c中还剩下的两个函数添点儿东西。第一个函数是:extern void HalLedInit( void );
我们填成:
void HalLedInit (void)
{
#if (HAL_LED == TRUE)
  P0DIR |= (HAL_LED_1 + HAL_LED_2):
#endif /* HAL_LED */
}
目的是把P0口的最低两位设置成输出方式。
第二个函数是:extern uint8 HalLedSet( uint8 led, uint8 mode );
我们填进去内容后函数如下:
void HalLedSet (uint8 leds, uint8 mode)
{
  if(leds == HAL_LED_1)
  {
    if(mode ==  HAL_LED_MODE_ON)
      P0_0 = 0;
    else
      P0_0 = 1;
  }
  if(leds == HAL_LED_2)
  {
      if(mode ==  HAL_LED_MODE_ON)
      P0_1 = 0;
    else
      P0_1 = 1;
  }

}这个函数目的是来设置两个led 是亮还是灭,仅此而已,没别的功能了。这里我们把函数实现写的最通俗易懂了,一看就明白了吧。需要注意的是我的板子是吸收电流的方式点亮LED的,所以ON的时候是0,OFF的时候是1.
搞了这么长了,一直在改程序,下载看看吧,基本想法是不是可以。先在GenericApp.c文件中的这个位置
添加一个点亮LED1的函数,试试看网络状态变化后灯亮不亮,改完后,make,下载运行,一切ok,coordinator的基本设想没啥问题。
case ZDO_STATE_CHANGE:
          HalLedSet(HAL_LED_1,HAL_LED_MODE_ON);
4)下面开始搞uart。
最近很多人在论坛上讨论uart的一些事情,协议栈把这个uart弄的怪麻烦的。其实协议栈的代码挺好的,只不过转来转去有些晕。我把uart的东西改简单了,直接操作寄存器,写uart用查询方式,uart来数据了用中断方式好了。开始工作吧。
先打开hal_uart.h和hal_uart.c看看吧。Hal_uart.h里面uart的波特率、停止位、奇偶校验、流控制和一些结构体,以及一些初始化函数的声明。在hal_board_cfg.h则有是否使用uart的#define,我们先把uart给#define禁用看看咋样吧。
#ifndef HAL_UART
#if (defined ZAPP_P1) || (defined ZAPP_P2) || (defined ZTOOL_P1) || (defined ZTOOL_P2)
#define HAL_UART TRUE
#else
#define HAL_UART FALSE
把这里的#define HAL_UART TRUE中的TRUE也改成FALSE,这样死活不会启动uart了。编译一下,不出意外的话,不会有错误哦。Make一下看看?没问题吧。好,现在把hal_uart.h和hal_uart.c都给它 掉,再make,OK依旧。如此,我们已经把关于uart的东西统统干掉了。
     不行啊,我们要用uart发数据到PC呢,这么搞uart就不能用了。没关系,我们自己写一个自己的uart.h和uart.c,直接操作硬件寄存器,绕过协议栈那些复杂的过程,当然了,这么处理起来uart肯定没有协议栈中的用法灵活,协议栈MT功能也不能用了,但是这么做会很直观。说实话,让我写一个能同TI源码比拟的程序出来,真的是没那个水平。哈哈 。
到这里该去看一下CC2430的user guide了。看看如何操作串口吧,如果你能找到裸机操作串口的例子,那你就省心了。
通过看user guide,写出uart的初始化函数:
void MyUartInit(void)

P0SEL |= 0x0C;        //P0_2,P0_3 is RXD and TXD
PERCFG &= ~ 0x01;//P0口选择为第一备选功能
//115200 baud rate.
U0GCR = 0x0B;
U0BAUD = 0xd8;
U0CSR |= 0x80;
U0UCR = 0x82;

这里我初始化串口为8位数据,1位起始,1位停止,其它校验和流控制都没有。波特率115200.具体每个寄存器初始化内容请同学们看CC2430的手册吧。我这里不给出来了。
再写一个uart的写数据函数:
void MyUartWrite(uint8 *pString,uint8 length)
{
  uint8 i;
for(i=0;i<length;i++)
  {
    U0DBUF = pString;
    while(U0CSR & 0x01);
  }
}
这个函数把一个数组中的数据写到串口去(如果数组是字符码可以直接在PC端的调试助手显示了,如果是数据,就要自己做一个ASCII码转换的东东了,我就不写了),程序中最后的while语句时判断之前写到串口BUF中的那个数据是不是发送出去了,要是没发出去就写新数据过去就不对了哦。
这两个函数我写在一个叫做my_uart.c的文件中,再写一个叫做my_uart.h的头文件,声明一下这两个函数,#include 一下"hal_board.h",好能使用uint8这样的系统预定义数据类型,然后把这两个文件加入到项目组中去就OK了,用到的地方主意include一下这个头文件。
好啦,到这里自己的一个串口驱动文件就做好了,编译一下?OK不?

接下来干什么?
按照最初设想,coord没有键盘,也不用串口,只是在协调器网络建立后点亮一个led 指示,之后每过一段时间(我的例子用5秒钟)广播一个字符串数据出去,仅此而已。这部分前面已经实现了。
前面的第一个大部分,(GenericApp.c文件的解释和处理),我们把程序删除的不剩下什么东西了,这个是为协调器准备的,接下来我们还要准备一个路由器的GenericApp.c,不过不能叫这个名字啦。其实也可以不单独写一个程序,而是在原有GenericApp基础上通过条件编译来解决协调器和路由器程序不一样的问题。不过为了让大家看着方便,我再另外搞一个程序吧。取名字叫做RoutGenericApp.c.在这个文件中只保留了函数
void GenericApp_MessageMSGCB( afIncomingMSGPacket_t *pkt )
{
  switch ( pkt->clusterId )
  {
    case GENERICAPP_CLUSTERID:
      MyUartWrite(pkt->cmd.Data,pkt->cmd.DataLength);  
     break;
  }
}
该函数在AF_INCOMING_MSG_CMD:消息响应中调用,其主要功能就是将协调器发过来的数据送到串口显示。
另外还需要在该文件中调用一下串口初始化函数MyUartInit()。
 楼主| kennan 发表于 2010-11-28 10:36:47 | 显示全部楼层
回复 2# kennan


   
好了,经过一番折腾,终于实现了最初的设想:不要键盘;直接操作LED;绕过协议栈的UART操作,直接操作寄存器。
在最后下载板子测试的时候浪费了一点儿时间,因为原来的GenericApp.c文件里面对于数据发送方式(广播、组播、单播这些)、端点号、短地址这些原来设置是这样的:
GenericApp_DstAddr.addrMode = (afAddrMode_t) AddrNotPresent;
GenericApp_DstAddr.endPoint = 0
GenericApp_DstAddr.addr.shortAddr = 0;
所以导致协调器发送的数据路由器收不到,刚开始忽略了这个地方,花了半天找错,以为板子出硬件问题了呢。
修改成如下:
GenericApp_DstAddr.addrMode = (afAddrMode_t)AddrBroadcast;
  GenericApp_DstAddr.endPoint = GENERICAPP_ENDPOINT;
  GenericApp_DstAddr.addr.shortAddr = 0xffff;
问题就解决了。
剩下的就是再清理清理key,led这些东东在hal_board_cfg.h等文件中遗留下的多余的东西。我就不多说了。
实际上用TI提供的基本驱动来工作还是比较舒服的,虽然有些地方很恶心,但是确实会带来一定的方便,我这篇文档的初衷还是希望我们这样的新手们能通过例子的更改熟悉协议栈的硬件驱动,并且给出一个最简单的通信范例,希望能为zigbee的学习带来一点儿帮助吧。

唉,又过了0点了,该睡觉了。这个修改试验加文档花了两个晚上多半个白天。可能写的有些乱,为了让新手同学们能够亲自实验,我把修改后的项目文件(整个ZStack-1.4.3-1.2.1文件夹)压缩了一起上传到www.feibit.com的论坛上,有需要的可以直接编译使用了。还有别的问题可以到飞比论坛短消息我。修改的源码到共享资源找把,这里上传不了,太大了。
 楼主| kennan 发表于 2010-11-28 10:47:02 | 显示全部楼层
删除了document和tool文件夹,压缩后可以上源码了。


outman 发表于 2010-11-28 11:09:45 | 显示全部楼层
毛毛老师的大作,搬个板凳先~
outman 发表于 2010-11-28 12:11:15 | 显示全部楼层
过了零点还在挑灯夜战呢,辛苦啦。。。
实际上rayfile服务器是可以传几十M的文件呢,应该是没问题的。tool文件夹里有编译需要的文件,删了可能会出问题哦
 楼主| kennan 发表于 2010-11-28 13:48:05 | 显示全部楼层
hah ,tool文件夹好找,我没动过那里面的东西。估计大家不难找到。
cyril3 发表于 2010-11-28 20:41:13 | 显示全部楼层
我看懂了楼主的意思,楼主的做法是直接将OSAL的HAL层的大部分功能去掉,绝大部分的硬件操作都通过寄存器直接完成。但是我完全不能明白楼主这样做的意义。我认为OSAL的存在是为开发者提供一个代码可重用的平台,使开发者能够将注意力完全集中在功能,而不是繁琐的寄存器上。而HAL(硬件抽象层)作为OSAL的一部分,它将嵌入式中大部分经常使用到的设备的功能通过函数的形式提供给开发者。所以,我认为楼主这种精简行为是没有必要的。
不过,也许有人会问:“如果我的设备确实就这么简单,难道我精简一下就不好吗?”我想说,如果你十分确定你的项目完全不需要拓展功能,那我没有什么异议。但是一旦需要添加硬件实现功能拓展的话,你是打算继续自己操作寄存器呢,还是用回HAL?继续操作寄存器意味着你将要重新面对硬件底层,而将优秀的HAL弃之不顾。回到HAL意味着之前代码努力会付之东流。无论哪种情况,都是违背软件工程的基本思想,已经提供好的代码复用机制被亲手打破。我想这是一个不明智的做法。
顺便说一句,我认为楼主的简化只是做了代码的简化,对于程序的执行来说,优化作用不大。
 楼主| kennan 发表于 2010-11-28 21:30:00 | 显示全部楼层
我看懂了楼主的意思,楼主的做法是直接将OSAL的HAL层的大部分功能去掉,绝大部分的硬件操作都通过寄存器直接 ...
cyril3 发表于 2010-11-28 20:41



    ^_^,从使用来说,当然是协议栈原来的东西完整啊。做这个主要是很多新手,尤其是从单片机那边转过来的,看硬件操作哪些代码迷糊,迟迟不能入门,这么做的目的是让新手们可以按照单片机的逻辑来尽快上手。真是做应用了,也就入门了,不需要这么干了。
 楼主| kennan 发表于 2010-11-28 21:38:14 | 显示全部楼层
我看懂了楼主的意思,楼主的做法是直接将OSAL的HAL层的大部分功能去掉,绝大部分的硬件操作都通过寄存器直接 ...
cyril3 发表于 2010-11-28 20:41



    很多新手拿到的板子和TI的完全不一样,结果导致点亮一个灯,处理一个按钮都很困难,结果也就没法子继续实验无线数传了。不是经常有人在群里问如何才能把按键去掉,不用按键就能自动发数据接数据吗?还有很多问串口的,一直从PC端收不到串口数据。那么这个文档一方面让新手们可以简单地改改程序点亮自己板子上的灯,演示一下通信,提高一下信心哦。已经入门的选手们没必要看这个帖子哦。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表