3. ZigBee 协议栈基础:Z-Stack 的下载、安装与工程结构

好,咱们进入第三章。这一章是纯基础,但也是你后面所有实战的根基。说白了,ZigBee 协议栈就是一套帮你把无线通信做好的软件框架。你不需要从零去写 MAC 层、网络层,TI 已经帮你封装好了,就是 Z-Stack。

我记得刚接触 ZigBee 那会儿,面对一堆文件夹和代码,完全不知道从哪下手。后来摸透了它的套路,才发现其实结构非常清晰。今天我就带你把它拆开看看。

3.1 Z-Stack 的下载与安装

先搞定工具。Z-Stack 是 TI 官方的协议栈,目前主流版本是 Z-Stack 3.0.x 和 Z-Stack Home 1.2.x。我建议你直接用 Z-Stack 3.0.2,它支持最新的 ZigBee 3.0 规范,兼容性最好。

下载渠道: 去 TI 官网搜 "Z-Stack 3.0.2",或者用我的网盘链接(课程资料里有)。安装路径不要带中文和空格,我习惯放在 C:\TI\Z-Stack 3.0.2。

安装步骤很简单,双击 exe,一路 Next 就行。但要注意一点:安装前关掉杀毒软件,否则有些文件会被误删。我曾经遇到过安装完编译报错,查了半天发现是 dll 被隔离了,很坑。

3.2 协议栈工程结构解析

安装完后,打开 C:\TI\Z-Stack 3.0.2\Projects\zstack\Samples\SampleApp\CC2530DB,用 IAR 打开 SampleApp.eww。你会看到左侧 Workspace 里有一堆文件夹。别慌,我带你一层层剥开。

Z-Stack 的架构严格遵循 ZigBee 协议分层,从下到上依次是:

层级 文件夹/模块 作用
物理层 (PHY) HAL 硬件抽象层,管射频、GPIO、UART、定时器
MAC 层 MAC 信道访问、CSMA/CA、信标管理
网络层 (NWK) NWK 组网、路由、地址分配
应用支持子层 (APS) APS 数据绑定、端点管理、安全
ZigBee 设备对象 (ZDO) ZDO 设备发现、绑定管理、网络管理
应用层 (APL) App 你的用户代码,比如 SampleApp

你看,每一层都对应一个文件夹。我刚开始做项目时,总喜欢在 App 层里直接操作寄存器,后来发现这样破坏了分层结构,移植性很差。正确的做法是:通过 HAL 接口访问硬件,通过 APS 接口发送数据。

3.2.1 HAL 层

HAL 是硬件抽象层,说白了就是把芯片的寄存器操作封装成函数。比如你要控制一个 LED,直接调 HalLedSet() 就行,不用管具体是哪个 GPIO 口。我在项目中遇到过一个问题:换了块板子,LED 引脚变了,但只要改 HAL 层的配置,上层代码一行都不用动。这就是抽象的好处。

3.2.2 MAC 层

MAC 层负责信道访问和帧收发。它实现了 CSMA/CA 机制,也就是发数据前先听信道是否空闲。你想想看,如果两个设备同时发数据,就会碰撞。MAC 层会自动退避重发,你基本不用管。

3.2.3 NWK 层

网络层是 ZigBee 的核心。它负责设备入网、地址分配、路由发现。比如协调器启动后,NWK 层会自动建立一个网络,其他设备加入时,NWK 层会分配一个 16 位短地址。我调试时经常用抓包工具看 NWK 层的命令帧,能快速定位组网失败的原因。

3.2.4 APS 层与 ZDO

APS 层提供端到端的数据传输服务,支持单播、广播、组播。ZDO 则负责设备管理和服务发现。比如你要知道网络里有哪些设备,就通过 ZDO 发送 Device_annce 请求。

重点: 你写的应用代码主要在 App 文件夹里。但理解下面几层的工作原理,能帮你快速定位问题。比如数据发不出去,可能是 NWK 层路由没建立,也可能是 MAC 层信道冲突。

3.3 OSAL 操作系统抽象层原理

Z-Stack 不是跑在 RTOS 上的,它用的是自己的一套轻量级调度机制——OSAL(Operating System Abstraction Layer)。你把它理解成一个微型操作系统就行。

OSAL 的核心是 任务轮询。它维护一个任务列表,每个任务有一个事件标志位。主循环不断检查哪个任务有事件待处理,然后调用对应的处理函数。没有中断抢占,没有时间片轮转,就是简单的轮询。

为什么会这样设计?因为 ZigBee 节点资源有限(CC2530 只有 8KB RAM),跑一个完整的 RTOS 太奢侈。OSAL 足够轻量,而且能满足无线通信的实时性要求。

3.3.1 任务与事件调度机制

OSAL 的调度流程可以用下面这张图表示:

OSAL 任务调度流程图 主循环入口 检查所有任务事件标志 有事件? 是 调用事件处理函数 处理完成,继续轮询 否 说明:OSAL 主循环不断轮询所有任务,有事件就处理,没事件就继续查,永不阻塞

具体来说,每个任务都有一个 uint16 events 变量,每一位代表一个事件。比如 SYS_EVENT_MSG 是 0x8000,表示有消息到来。OSAL 主循环里调用 osal_run_system(),它会遍历任务列表,找到优先级最高的、有事件的任务,然后调用其处理函数。

我举个例子。你在 SampleApp 里会看到这样的代码:

// 任务初始化函数
void SampleApp_Init( uint8 task_id )
{
    SampleApp_TaskID = task_id;
    // 注册端点、初始化硬件等
}

// 事件处理函数
uint16 SampleApp_ProcessEvent( uint8 task_id, uint16 events )
{
    if ( events & SYS_EVENT_MSG )
    {
        // 处理消息
        return (events ^ SYS_EVENT_MSG);
    }
    if ( events & SAMPLEAPP_SEND_PERIODIC_MSG_EVT )
    {
        // 定时发送数据
        osal_start_timerEx( SampleApp_TaskID,
                            SAMPLEAPP_SEND_PERIODIC_MSG_EVT,
                            SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT );
        return (events ^ SAMPLEAPP_SEND_PERIODIC_MSG_EVT);
    }
    return 0;
}

你看,SampleApp_ProcessEvent 就是任务的事件处理函数。OSAL 检测到有事件,就调它。处理完后,返回未处理的事件标志。如果返回 0,表示全部处理完毕。

避坑指南: 我曾经在事件处理函数里写了一个 while 死循环等待某个标志,结果整个系统卡死了。因为 OSAL 是单线程轮询,你在一个任务里死等,其他任务永远得不到执行。正确的做法是用状态机或定时器,不要阻塞。

3.3.2 定时器与消息

OSAL 提供了两个重要的机制:定时器和消息队列。

  • 定时器: 用 osal_start_timerEx() 启动一个定时器,到期后会自动设置对应的事件标志。比如你想每 5 秒发一次数据,就在事件处理函数里重新启动定时器。
  • 消息: 用 osal_msg_send() 发送消息,接收方在事件处理函数里用 osal_msg_receive() 取出。消息是动态分配的,用完后要 osal_msg_deallocate() 释放,否则内存泄漏。

我刚开始用 OSAL 时,总忘记释放消息,结果跑了一晚上,节点死机了。后来养成习惯:每次收到消息,处理完立刻释放。

3.4 本章小结

这一章我们讲了 Z-Stack 的下载安装、工程结构(HAL、MAC、NWK、APS、ZDO),以及 OSAL 的任务调度机制。你不需要记住每一层的细节,但一定要理解:Z-Stack 是一个分层架构,OSAL 是一个事件驱动的轮询系统。后面写代码、调 bug,都会用到这些知识。