Skip to content

实验四:Z-Stack 拓扑实验 ​

一句话理解

ZigBee 网络的拓扑信息就藏在每个节点的 3 张表里:_NIB(我是谁)、关联表(谁是我的父/子)、邻居表(我能听到谁)。本实验用一个 topo.c 小模块把 3 张表读出来,分片从串口打印,再用打印结果手绘一张网状(Mesh)拓扑图——实线是父子、虚线是邻居,出现虚线回路就说明网络具备网状特征。

前置知识 ​

本实验建立在两个前置之上:

  1. 2.3 ZigBee 3.0 组网实验:协调器建网、路由器/终端入网、角色配置(CoordinatorEB / RouterEB / EndDeviceEB);
  2. 实验一:Z-Stack 简单组网实验:入网成功后读取并打印 PAN ID / 短地址 / 父节点。

本实验的增量是:不只打印"我的父节点",而是把节点内部的三张表全部读出来,形成一份完整的"拓扑报告",供手工绘制网络拓扑图。

实验目的 ​

  1. 学会在 IAR 中修改并编译一个 Z-Stack 工程,掌握固件下载流程;
  2. 理解 ZigBee 网络拓扑信息在固件内部是怎么存放和读取的(_NIB、关联表 AssociatedDevList[]、邻居表 NeighborTable);
  3. 认识协调器、路由器、终端三种设备类型的角色差异,理解**网络深度(Depth)**的含义;
  4. 理解网状(Mesh)拓扑的本质:路由器之间也存在链路,形成到协调器的冗余路径;
  5. 用串口终端采集各节点的拓扑报告,手工绘制网状拓扑图,区分父子关系(实线)与邻居关系(虚线)。

实验器材 ​

器材数量说明
CC2530 开发板(板载 USB 转串口)5 块1 协调器 + 2 路由器 + 2 终端
USB 线(A 转 Mini / Micro-B,按板子接口选)5 根供电 + 串口(板载转串口,无需外接模块)
USB Hub(带独立供电更稳)1 个让 5 块板同时接在 PC 上
仿真器(CC Debugger / 节点自带下载口)1 个烧写固件
IAR EW for 8051 + 串口助手1 套编译工程、观察串口
方格纸、铅笔、直尺若干拓扑图的产出靠它

实验原理 ​

三种设备 ​

🏛️
协调器 Coordinator
组建并管理网络,全网有且只有一个,短地址恒为 0x0000,深度 0
🔄
路由器 Router
可被其他节点选为父节点,转发数据、参与路由,深度 ≥ 1
🍃
终端 End Device
只能做叶子节点,不能转发数据、不能做父节点,可休眠

关键:路由器和终端都能"入网",但只有路由器能当"二传手"——网状拓扑正是靠路由器互连形成的,本实验要用的正是这一点。

网络深度 Depth ​

深度 = 该节点到协调器需要经过的跳数:

            Depth = 0
        ┌──────────────┐
        │    协调器     │  0x0000
        └──┬────────┬──┘
           │        │
   Depth=1 │        │ Depth=1
      ┌────▼───┐ ┌──▼─────┐
      │ 路由器A │ │ 路由器B │
      └────┬───┘ └────────┘
           │ Depth=2
      ┌────▼───┐
      │  终端C  │
      └────────┘

协调器 Depth=0,直接挂协调器的节点 Depth=1,挂路由器 A 的终端 C Depth=2。

三张表——本实验在固件里读的就是它们 ​

每个节点内部维护着以下数据,topo 模块要读的就是它们:

名称里面有什么拓扑报告里输出
_NIB(网络信息库)本机短地址、PANID、信道、深度、协调器地址ShortAddr / PANID / Channel / Depth / CoordAddr
关联表 AssociatedDevList[]和"我"有正式关系的设备:父节点 + 子节点Parent / Children
邻居表 NeighborTable只要"我能听到"的设备(含无亲属关系的)Neighbors + 每条链路的 LQI

本实验最关键的一点:邻居 ≠ 父子

一个节点出现在你的邻居表里,只说明"你能听到它",不表示它是你的子节点。 判断父子关系必须看关联表(报告里的 Parent / Children),不能看邻居表。 拓扑图上那些"虚线",正是从邻居表里找出来的"非父子邻居"。

链路质量 LQI ​

LQI(Link Quality Indicator) 表示一条链路的通信质量,0~255,越大越好:

  • LQI 高(200 以上):链路很好;
  • LQI 低(100 以下):容易丢包,需要重传;
  • LQI 接近 0:接近接收极限,随时可能断链。

LQI 会随距离变远、墙体遮挡、Wi-Fi/微波炉干扰而下降。ZigBee 网络层把 LQI 折算成"路由代价"来选路——这正是网状网络自动选路的原因。本实验只把 LQI 当作相对好坏的参考,标注在图上。

两种地址 ​

地址类型长度特点
短地址16 位,如 0x796F入网时由父节点分配,可能变化;协调器固定 0x0000
IEEE 地址64 位,如 00124B0001A2B3C5芯片出厂固化,全球唯一,永不改变

画拓扑图标注短地址即可;要把节点和上次实验对上号,要靠 IEEE 地址。

什么是网状(Mesh)拓扑 ​

星型树型网状(本实验)
结构所有节点直连协调器分层中转路由器之间也互连
到协调器的路径1 条1 条≥ 2 条(冗余)
一个路由器坏了它自己掉线它下面的子树全掉自动改走别的路由器
邻居表里有什么基本只有协调器父节点 + 子节点父 + 子 + 其他路由器
拓扑图特征只有实线,放射状只有实线,分层虚线构成回路

本实验要搭出的"最小网状":

                  路由器 A
                 ╱        ╲
            实线 ╱          ╲ 实线      ← 父子关系(每个节点只有一个父节点)
               ╱            ╲
           协调器 ·········· 路由器 B
                  虚线(邻居,非父子)    ← 冗余路径

   3 个具备路由能力的设备两两互连,
   所以路由器 A 到协调器有两条路可走:A→协调器,或 A→B→协调器。

判据速记:图上出现"不是父子、但互为邻居"的虚线,并且这些虚线构成回路(环),就说明网络具备了网状特征。

固件准备(核心动手环节) ​

你要做的事情一览 ​

节点固件没有预先烧录,你需要:

① 把 topo.c / topo.h 加入 IAR 工程
        ↓
② 修改 zcl_samplesw.c 的 5 处(这是你的编码任务)
        ↓
③ 编译 3 个配置:CoordinatorEB / RouterEB / EndDeviceEB
        ↓
④ 烧写 5 块板
        ↓
⑤ 分级调试:先协调器 → 再加路由器 → 最后加终端

涉及的文件 ​

工程位置:Z-Stack 3.0.1\Projects\zstack\HomeAutomation\SampleSwitch\

文件你要做什么
Source\topo.c / topo.h加入 IAR 工程(内容不用改,但请先读懂,见下)
Source\zcl_samplesw.c修改 5 处 ← 本次编码任务
CC2530DB\SampleSwitch.ewwIAR 工作区,用 IAR 打开它

如果老师只发给你 topo.c / topo.h 两个文件,先把它们复制到 Source\ 目录下。

直接下载:⬇️ topo.h ⬇️ topo.c

topo 模块在干什么(动手改代码前务必先看) ​

Topo_Print() 只做一件事:读 3 张表,拼成文本,从串口打出去。

报告字段代码里从哪里读
ShortAddr / PANID / Channel / Depth / CoordAddr_NIB.nwkDevAddress / nwkPanId / nwkLogicalChannel / nodeDepth / nwkCoordAddress
IEEEAddrNLME_GetExtAddr()
Role编译宏 ZDO_COORDINATOR / RTR_NWK(由链接配置决定)
Parent / Children遍历 AssociatedDevList[],用 AssocFindDevice(i) 取每一项,看它的 nodeRelation
Neighbors + LQINLME_GetRequest(nwkNumNeighborTableEntries, 0, &n) 取条数;NLME_GetRequest(nwkNeighborTable, i, &entry) 逐条取地址和 entry.linkInfo.rxLqi

v3 版做了 3 件"防坑"的事(是本实验最有价值的知识点):

设计解决什么问题
分片发送(TOPO_LINES_PER_PASS,默认每节拍 3 行)整份报告约 600 字节,而串口 DMA 发送缓冲只有约 256 字节,HalUARTWrite() 缓冲一满就返回 0 静默丢数据。分片后每次突发最多约 165 字节,缓冲永不溢出
Topo_Flush() 检查返回值 + HalUARTPoll() 排空重试兜底保险:万一没写进去,主动让 DMA 排空再重试
诊断面包屑(TOPO_DEBUG_STEPS,[TOPO] 1 enter / 2 joined / 3 identity ok / 4 assoc ok / 5 done)报告被分片后,肉眼能确认打印进行到哪一步,排查"停在一半"的假象

未入网提示也升级了:不再只说"not joined",而是打印 TOPO: not joined yet. Role=... devState=...——直接告诉你是烧错固件(Role 不对)还是建网失败(devState 卡住)。

另外两个函数负责串口终端命令:

函数运行位置干什么
Topo_RxByte(ch)可能在中断里只记录收到的命令字符,绝不打印
Topo_Tick()任务上下文取出命令执行;把报告分成几个节拍逐步发完;没有命令就按周期自动打印

为什么要分成两个函数?因为中断里不能长时间占用串口(会阻塞协议栈)。这个"中断只记录、任务再打印"的分工,是本实验一个值得注意的设计点。

节拍周期 TOPO_TICK_MS(默认 100 ms)和分片行数 TOPO_LINES_PER_PASS(默认 3)都定义在 topo.h 里;报告周期默认 10 秒,可在串口终端里用 + / - 实时调整。

内存优化说明(v3 版)

CC2530 只有 8 KB XDATA,SampleSwitch 工程几乎用满了,旧版 topo 模块一加入工程就会出现内存上限(Segment XDATA_I ... is too long)链接错误。v3 延续 v2 的 3 处省内存设计:

做法省下
不用十六进制查找表,改 Topo_PutHexDigit() 用算术算17 字节
只声明一个需要初始化的变量(其余靠零初始化)2 字节
输出缓冲只留 16 字节,装满就立刻发出去("分片发送"而不是"攒够一整行")48 字节

整个模块 XDATA 占用仅约 22 字节。对外接口完全不变(Topo_Print / Topo_RxByte / Topo_Tick、命令 t/p/+/−/h),zcl_samplesw.c 的 5 处修改也不需要变,直接替换这两个文件即可。

步骤一:把 topo.c 加入工程 ​

  1. 用 IAR 打开 CC2530DB\SampleSwitch.eww;
  2. 左上角配置下拉框选 CoordinatorEB;
  3. 在左侧工程树里找到 SampleSwitch – Source 分组;
  4. 右键该分组 → Add → Add Files…,选中 Source\topo.c,确定(topo.h 一般自动被索引,若 IAR 要求手动加头文件,一并加入);
  5. IAR 的 .ewp 只有一份文件列表——只需添加一次,三个配置都会编译它。

步骤二:修改 zcl_samplesw.c(5 处) ​

打开 Source\zcl_samplesw.c,按下面 5 处修改。行号可能略有偏差,用搜索关键字定位更可靠。 ⚠️ 不要在这个文件里输入中文(IAR 对 UTF-8 支持不佳),注释用英文。

改动 1 —— 加头文件

找到:

c
#include "zcl_samplesw.h"

改为:

c
#include "zcl_samplesw.h"
#include "topo.h"

改动 2 —— 加拓扑模块的事件宏

找到:

c
#define SAMPLESW_TOGGLE_TEST_EVT   0x1000

改为:

c
#define SAMPLESW_TOGGLE_TEST_EVT   0x1000
/* Topology module event: drives both the terminal commands and the periodic report. */
#define SAMPLESW_TOPO_EVT          0x2000

改动 3 —— 把串口初始化和定时器挪到 #ifdef ZDO_COORDINATOR 外面(最容易踩的坑!)

⚠️ 先看完再动手——这是本实验最容易踩的坑

工程原始代码把 zclSampleSw_InitUart() 放在了一个条件编译块里:

c
  zdpExternalStateTaskID = zclSampleSw_TaskID;

#ifdef ZDO_COORDINATOR              ← 条件从这里开始
  // Init Uart
  zclSampleSw_InitUart();           ← 串口初始化在里面!
  ZDO_RegisterForZDOMsg(...);
  bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_FORMATION | ... );
#else
  bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_STEERING | ... );
#endif

ZDO_COORDINATOR 只在协调器固件里定义(见配置 f8wCoord.cfg),所以在路由器(RouterEB)和终端(EndDeviceEB)里这一整块都被编译掉了——串口从来没被初始化过,既没有回声、也没有任何输出。

编译时会看到这条警告,它就是在提示这个问题:

Warning[Pe177]: function "zclSampleSw_InitUart" was declared but never referenced

看到 Pe177 就是没改对,路由器一定没有串口输出。

找到这一整块(大约在 zclSampleSw_Init() 的中后段):

c
  zdpExternalStateTaskID = zclSampleSw_TaskID;

#ifdef ZDO_COORDINATOR

  // Init Uart
  zclSampleSw_InitUart();

  ZDO_RegisterForZDOMsg ( zclSampleSw_TaskID, Device_annce );
  bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_FORMATION |
                          BDB_COMMISSIONING_MODE_FINDING_BINDING );
#else

  bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_STEERING |
                          BDB_COMMISSIONING_MODE_FINDING_BINDING );
#endif

改为——把串口初始化和我们的定时器一起挪到 #ifdef 前面(ZDO_RegisterForZDOMsg 和 bdb_StartCommissioning 保持原位不动):

c
  zdpExternalStateTaskID = zclSampleSw_TaskID;

  /* ---- These two MUST run for every device role (coordinator, router, end
   *      device).  In the original project the UART init sat inside
   *      "#ifdef ZDO_COORDINATOR", so routers and end devices had no serial
   *      output at all (compiler warning Pe177). ---- */

  // Init Uart
  zclSampleSw_InitUart();

  // Start the topology module tick (drives the terminal commands + periodic report)
  osal_start_timerEx( zclSampleSw_TaskID, SAMPLESW_TOPO_EVT, TOPO_TICK_MS );

#ifdef ZDO_COORDINATOR

  ZDO_RegisterForZDOMsg ( zclSampleSw_TaskID, Device_annce );
  bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_FORMATION |
                          BDB_COMMISSIONING_MODE_FINDING_BINDING );
#else

  bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_STEERING |
                          BDB_COMMISSIONING_MODE_FINDING_BINDING );
#endif

验收方法:改完 Rebuild All,Pe177 警告必须消失。警告还在 = 还有没挪出来的,路由器/终端一定没有串口输出。


改动 4 —— 在事件循环里处理拓扑事件

找到 zclSampleSw_event_loop() 里最后一个事件分支的结尾与函数返回:

c
  if ( events & SAMPLEAPP_REJOIN_EVT )
  {
    ...
    return ( events ^ SAMPLEAPP_REJOIN_EVT );
  }

  // Discard unknown events
  return 0;

在两者之间插入:

c
  if ( events & SAMPLESW_TOPO_EVT )
  {
    Topo_Tick();   /* handles a pending terminal command and the periodic report */
    osal_start_timerEx( zclSampleSw_TaskID, SAMPLESW_TOPO_EVT, TOPO_TICK_MS );
    return ( events ^ SAMPLESW_TOPO_EVT );
  }

⚠️ osal_start_timerEx 那一行不能漏

这个定时器是一次性的,不重新装载就只会响应一次——这是本实验最容易犯的错误。


改动 5 —— 把串口收到的字节交给拓扑模块(终端输入的关键)

找到 zclSampleSw_UartCB():

c
static void zclSampleSw_UartCB(uint8 port, uint8 event)
{
  uint8 rxLen = Hal_UART_RxBufLen(HAL_UART_PORT_0);

  if(rxLen != 0)
  {
    HalUARTRead(HAL_UART_PORT_0  ,  zclSampleSw_UartBuf , rxLen);
    HalUARTWrite(HAL_UART_PORT_0 ,  zclSampleSw_UartBuf , rxLen);
  }
}

改为:

c
static void zclSampleSw_UartCB(uint8 port, uint8 event)
{
  uint8 rxLen = Hal_UART_RxBufLen(HAL_UART_PORT_0);

  if(rxLen != 0)
  {
    uint8 i;

    HalUARTRead(HAL_UART_PORT_0  ,  zclSampleSw_UartBuf , rxLen);

    /* feed the received bytes to the topology terminal */
    for ( i = 0; i < rxLen; i++ )
    {
      Topo_RxByte( zclSampleSw_UartBuf[i] );
    }

    HalUARTWrite(HAL_UART_PORT_0 ,  zclSampleSw_UartBuf , rxLen);
  }
}

这个回调可能运行在中断上下文,所以只调用 Topo_RxByte()(只记录),绝不在这里打印。

步骤三:编译三个配置 ​

配置用途输出
CoordinatorEB协调器CC2530DB\CoordinatorEB\Exe\SampleSwitch.hex
RouterEB路由器 A、BCC2530DB\RouterEB\Exe\SampleSwitch.hex
EndDeviceEB终端 C、DCC2530DB\EndDeviceEB\Exe\SampleSwitch.hex

每个配置的操作:下拉框选中配置 → Project → Rebuild All → 应 0 error 通过。

⚠️ CC2530DB 工程里一共有 5 个配置,别选错:CoordinatorEB / RouterEB / EndDeviceEB / RouterEB - OTAClient / EndDeviceEB - OTAClient。本实验只用前三个——后两个带 OTA 空中升级功能,内存占用更大。

省时间技巧:同组分工每人只编一个配置,把 Exe 目录下的 .hex 放到共享文件夹互相取用;第二次编译是增量编译,快很多。

步骤四:烧写五块板 ​

节点角色烧写文件
协调器CoordinatorCoordinatorEB\Exe\SampleSwitch.hex
路由器 ARouterRouterEB\Exe\SampleSwitch.hex
路由器 BRouter同上
终端 CEnd DeviceEndDeviceEB\Exe\SampleSwitch.hex
终端 DEnd Device同上

方法一(IAR 直接下载):选中对应配置 → Project → Download and Debug → 下载完成后退出调试。 方法二(hex + Flash Programmer):用 SmartRF Flash Programmer 选择 .hex 文件烧写。

首次烧写前建议擦除 NV 存储(Flash Programmer 里选 Erase,或 IAR 里 Project → Download → Erase memory),否则旧的网络参数(PANID、短地址)会被恢复,影响观察。

⚠️ 不要一次把 5 块板全烧完再一起上电。 请按下面的分级调试法:先烧协调器 → 调通 → 再加路由器 A → 再加路由器 B → 最后加终端。一次只加一个节点,出问题立刻能定位。

编译 / 烧写常见错误 ​

报错或现象原因与处理
Fatal Error[Pe1696]: cannot open source file "topo.h"topo.h 不在 Source 目录里,或没加入工程
Error[Li005]: no definition for "Topo_Print" / "Topo_RxByte" / "Topo_Tick"topo.c 没加入工程(见步骤一)
Error[Pe020]: identifier "SAMPLESW_TOPO_EVT" / "TOPO_TICK_MS" is undefined改动 2 没做,或改动 1 的头文件没加
Warning[Pe177]: function "zclSampleSw_InitUart" was declared but never referenced串口初始化还在 #ifdef ZDO_COORDINATOR 里面 → 路由器和终端完全没有串口输出(协调器正常,所以很容易误判)。按改动 3 挪到 #ifdef 外面
只有协调器有串口输出,路由器/终端插上没任何反应同上,改动 3 没做对
编译通过、回声正常,但一直不出报告UART 没问题,是 Topo_Tick() 没被调用。做下面的「15 秒自检」
报告只打出一半,或每次缺的位置不一样串口 DMA 发送缓冲被塞满,HalUARTWrite() 静默丢数据。见「DMA 分片发送」说明
只能查询一次,之后没反应改动 4 里漏了 osal_start_timerEx 那一行
串口有自动输出,但打字没反应改动 5 没做
提示 flash 空间不足本模块只增加约 1~1.5 KB,正常不会。检查是否误开了其它可选功能
下载时报"找不到设备"检查 CC Debugger 驱动、排线方向、板子供电;先按一下 Debugger 上的 Reset
有的板子烧完就没反应可能烧错了配置(终端烧了 RouterEB 等),重新用正确配置烧写
Error[e16]: Segment XDATA_I ... is too long for segment definitionCC2530 的 8 KB XDATA 用满了。见下面的专项说明

如果报 Segment XDATA_I ... is too long ​

这个报错不是代码写错了,而是内存不够——CC2530 的 XDATA 只有 8 KB,要同时放全局变量和协议栈的各种缓冲区。典型报错长这样:

Error[e16]: Segment XDATA_I (size: 0x103 align: 0) is too long for segment definition.
            At least 0x3 more bytes needed.
The problem occurred while processing the segment placement command
"-Z(XDATA)XDATA_Z,XDATA_I=_XDATA_START-_XDATA_END", where at the moment of
placement the available memory ranges were "XDATA:1e00-1eff"

按下面顺序排查:

  1. 确认用的是最新版(v3)的 topo.c / topo.h。最新版把整个模块的 XDATA 占用压到了约 22 字节:输出缓冲只有 16 字节且装满就立刻发出去、不使用占 XDATA 的十六进制查表、只保留一个需要初始化的变量。旧版会多占约 100 字节——正是这个报错的来源。
  2. 确认编译配置没选错。只用 CoordinatorEB / RouterEB / EndDeviceEB,不要选 RouterEB - OTAClient / EndDeviceEB - OTAClient,那两个多带了 OTA 功能,内存占用更大。
  3. 把输出缓冲再调小(最后的手段):打开 topo.h,把 TOPO_LINE_BUF_LEN 由 16 改成 8。因为缓冲是"装满就发",调小不会截断任何内容,只是 UART 的调用次数略多。
  4. 如果以上都不行,说明工程本身已接近内存上限,请联系老师确认工程配置,或关闭本实验用不到的可选功能(如 OTA、Touchlink 等)。

为什么 topo.c 写得这么"抠"?——一个值得记住的设计

CC2530 只有 8 KB XDATA,而这个工程里协议栈和各种缓冲区已经用掉了绝大部分。所以这份代码刻意做了三件事来省内存:

做法省下
不用十六进制查表,改成一个 Topo_PutHexDigit() 用算术算17 字节
只声明一个需要初始化的变量(其余都靠零初始化)2 字节
输出缓冲只留 16 字节,装满就立刻发出去——靠"分片发送"而不是"攒够一整行"48 字节

第三条是最典型的嵌入式技巧:如果按"先拼好一整行、再一次性发出去"来写,缓冲就必须不小于最长的那一行(约 54 字节);而改成"满了就发",缓冲只要 16 字节就够,报告的行再长也不多占一个字节。代价是 UART 的调用次数略增,但在这个速度下完全看不出来。

这就是嵌入式开发和写 PC 程序最大的不同——每 1 个字节都要算。

15 秒自检:确认 5 处改动真的都在 ​

症状:能回显(发 x 有回显),但串口一直不出报告。

这说明串口完全正常,问题只有一个:Topo_Tick() 没有被调用。

在 IAR 里打开 zcl_samplesw.c,按 Ctrl+F 搜索 SAMPLESW_TOPO_EVT,应该出现 5 次:

#出现的位置属于哪处改动
1#define SAMPLESW_TOPO_EVT 0x2000改动 2
2zclSampleSw_Init() 里的 osal_start_timerEx(...),且在 #ifdef ZDO_COORDINATOR 外面改动 3
3zclSampleSw_event_loop() 里的 if ( events & SAMPLESW_TOPO_EVT )改动 4
4上面那个 if 块里的 osal_start_timerEx(...)改动 4(最容易漏的一行)
5上面那个 if 块里的 return ( events ^ SAMPLESW_TOPO_EVT );改动 4

少于 5 次 → 有改动没做。 补齐后必须 Rebuild All 再重新烧写。

再搜一次 Topo_Tick,应该出现在三处:topo.c(函数定义)、topo.h(函数声明)、zcl_samplesw.c(1 次调用,改动 4 里)。如果 zcl_samplesw.c 里搜不到 Topo_Tick,就是改动 4 没做。

最后还要确认 topo.c 真的加进了 IAR 工程:左侧工程树里 SampleSwitch – Source 分组下应该有 topo.c。

⚠️ 这里有个很隐蔽的坑:如果 topo.c 没加进工程,而改动 3 / 4 / 5 又恰好都没做,程序照样能编译通过、照样能烧写、回声也照样正常——因为整个工程里根本没有人调用 Topo_* 函数,编译器和链接器都不会报错。现象和现在一模一样。所以**"能编译通过"不等于"代码被用上了"**。

如果报告只打出一半,或者每次缺的位置都不一样 ​

这是本实验最隐蔽的一个坑,也是最有价值的一个知识点。

现象:

  • 收到 [TOPO] 1 enter … [TOPO] 3 identity ok,然后停在一行的中间;
  • 再发一次 t,这次却是从 [TOPO] 4 assoc ok 开始——正好接上一次断掉的地方;
  • 看上去像"卡死",其实程序完整跑完了([TOPO] 5 done、TOPO END 都出现过)。

原因:HalUARTWrite() 的底层是 HalUARTWriteDMA(),看它的源码:

c
if ((len + dmaCfg.txIdx[dmaCfg.txSel]) > HAL_UART_DMA_TX_MAX)
{
  return 0;          /* ← 缓冲满了就返回 0,数据被静默丢弃!不报错、不等待 */
}

三个关键点串起来就是事故:

#事实
1DMA 发送缓冲只有 约 256 字节(txBuf[2][HAL_UART_DMA_TX_MAX])
2缓冲一满,HalUARTWrite() 返回 0 并静默丢数据
3这个缓冲只由 HalUARTPoll() 排空,而 HalUARTPoll() 是在主循环里被调用的

我们在应用任务里一口气打印整份报告(约 600 字节)时,主循环根本没机会跑——所以缓冲一满,后面的输出全部丢失。报告越长丢得越多,而且每次丢的位置还不一样(取决于调用瞬间缓冲的状态)。

v3 的修法(两层保险):

① 主要手段:分片发送。 报告不再"一次性打出去",而是每个节拍只打 TOPO_LINES_PER_PASS(默认 3)行,由 Topo_ReportStep() 在连续几个节拍里逐步发完:

  • 每次 UART 突发最多约 165 字节,远低于约 256 字节的缓冲上限 → 缓冲永远不会满;
  • 一份完整报告约需 7 个节拍 × 100 ms ≈ 0.7 秒发完,肉眼几乎无感;
  • 这是根本不依赖 HAL 内部行为的做法,所以最可靠。

② 兜底手段:检查返回值。 Topo_Flush() 仍然检查 HalUARTWrite() 的返回值,万一还是没写进去,就主动 HalUARTPoll() 让 DMA 排空再重试。

要记住的一条通用经验:调用 HalUARTWrite() 一定要检查返回值,尤其在应用任务里连续输出大量数据时。忽略返回值 = 静默丢数据,而且现象会伪装成"卡死"或"随机丢字符",极难排查。

分级调试:先把协调器调通,再逐个加节点 ​

调试原则:一次只改一个变量,每加一个节点就验证一次。 不要 5 块板一起上电——出了问题分不清是谁的错。

第 0 步:烧录协调器前的三项检查 ​

检查说明
配置选对了吗?协调器必须用 CoordinatorEB
擦除 NV 了吗?Flash Programmer 里选 Erase,或 IAR Project → Download → Erase memory
COM 口是哪个?协调器插上后,设备管理器里新出现的那个

第 1 步:只给协调器上电,按顺序验证 4 件事 ​

(1)固件活着吗——看自动打印:上电后什么都不用做,等 10~15 秒,串口应该自动出现一份报告。出现了 → 直接做第(3)项;完全没有 → 做第(2)项。

(2)UART 通路测试(回声):在串口助手里随便发一个字符,比如 x。

现象说明下一步
看到回显 xUART 收发、波特率、驱动全都正常问题在拓扑模块 → 查改动 3 / 改动 4
什么都看不到UART 通路本身有问题波特率试 38400;换 COM 口;查驱动和 USB 线

为什么要先做回声测试? 回声是工程原有的功能(zclSampleSw_UartCB() 里那行 HalUARTWrite),和我们改的代码无关。所以它能干净地把"串口本身的毛病"和"我们代码的毛病"分开。这是最省时间的一步——否则你会去翻代码,其实只是波特率选错了。

(3)命令测试:

发什么应该看到什么
h命令列表 + 当前周期
t一份完整报告
pTOPO: automatic report interval = OFF

只要 h 能出来,就说明改动 1、2、3、4 全对了。

(4)核对内容——单协调器时期的期望值:

字段单独一个协调器时应该是如果不对
RoleCoordinator烧错配置了
ShortAddr0x0000同上
IEEEAddr16 位十六进制抄下来,这是这块板的唯一身份
PANID一个随机值(不是 0xFFFF)建网未成功
Channel11工程默认信道
Depth0
CoordAddr0x0000
Parent(none)协调器没有父节点,正确
Children0现在还没有节点入网,正确
Neighbors0(列表为空)正确

注意:报告是分几批刷出来的,不是一次打完。 为了让串口 DMA 缓冲不溢出(见前面的说明),一份报告会在约 0.7 秒内分 2~3 批出现。中间短暂停顿是正常现象,不是丢数据——等它打完再抄。

如果一直显示 TOPO: not joined yet. Role=... devState=...:节点还没把网络建起来。等 5~15 秒是正常的;超过 30 秒还没好,看那一行里的 Role:

显示含义怎么做
Role=Router 或 Role=End Device板上烧错固件了! 路由器和终端都不能建网,所以永远入不了网用 CoordinatorEB 重新编译 + 重新烧写
Role=Coordinator固件对,但建网失败先擦 NV 重烧;仍不行请找老师(可能与安全密钥配置有关)

devState= 是固件内部状态机的取值,对照表如下(取自 ZDApp.h 的 devStates_t):

值名称含义
0DEV_HOLD已初始化但未启动
1DEV_INIT已初始化,未连网
2DEV_NWK_DISC正在扫描可加入的网络 ← 路由器/终端刚上电时是这个
3DEV_NWK_JOINING正在加入网络
5DEV_END_DEVICE_UNAUTH已加入,等信任中心认证
6DEV_END_DEVICE终端已正常工作
7DEV_ROUTER路由器已正常工作
8DEV_COORD_STARTING协调器正在建网
9DEV_ZB_COORD协调器已建网
10DEV_NWK_ORPHAN与父节点失联
11DEV_NWK_KA正在给父节点发保活
12DEV_NWK_BACKOFF重入退避中(旧网络参数残留时会出现)

数值一直在变,说明节点还在反复尝试入网。

第 2 步:加第 1 个路由器 ​

建议先把路由器单独测一遍(协调器断电)。 把协调器的 USB 拔掉,只给路由器上电,发 t。你应该看到:

[TOPO] 1 enter
TOPO: not joined yet. Role=Router devState=2
  • [TOPO] 2 joined 不出现是正常的——没有协调器,它无处可入网;
  • devState=2(DEV_NWK_DISC)表示它正在扫描可加入的网络,也正常;
  • 但 Role=Router 这一项就验证了固件身份是对的(烧错配置这里会显示 Coordinator 或 End Device);
  • 顺便也验证了串口、命令、分片发送都正常。
  1. 给路由器烧 RouterEB,单独上电,等 20~30 秒;
  2. 给协调器发 t → Children 应该从 0 变成 1,并列出路由器的短地址;
  3. 给路由器发 t → 应该看到 Role: Router、Depth: 1、Parent: 0x0000;
  4. 交叉验证:协调器 Children 里的那个短地址 == 路由器的 ShortAddr?
现象处理
协调器的 Children 还是 0路由器没入网。给它断电重上电;确认协调器已建网
路由器等了 1 分钟以上还显示 not joined擦除该节点的 NV 再上电。 这块板以前入过别的网络时,会先尝试"重新加入旧网",而 Z-Stack 的重入退避最长可达 15 分钟(REJOIN_BACKOFF=900000 ms),课上等不起
路由器的 Depth 是 2,或 Parent 不是 0x0000上电顺序错了。全部断电,按顺序重来
路由器的 Role 显示 End Device烧错配置了,重新烧 RouterEB

为什么第 2 步就要做交叉验证? 因为"父节点说自己有个孩子"和"孩子说自己的父亲是谁"是两份独立的数据。两边对得上,才说明你读表的方式是对的(关联表的 Parent 和 Children 本来就是一对)。

第 3 步:加第 2 个路由器,验证"两两互为邻居"(本实验的成败判据) ​

  1. 保持协调器已建网;
  2. 把路由器 A、B 放在 1~3 m 内,与协调器构成三角形;
  3. 分别给 A、B、协调器发 t。
检查项应该是
协调器的 Neighbors同时包含 A 和 B
A 的 Neighbors同时包含 协调器 和 B
B 的 Neighbors同时包含 协调器 和 A
A、B 的 Parent都是 0x0000

四项全对 → 网络已经具备网状特征,可以开始画图了。

异常处理
A 的邻居表里没有 B把 A、B 再靠近(1 m 内),拔掉 USB 断电后重新上电
A 的 Parent 是 B(或反过来)上电顺序错了 → 全部断电;先只上协调器,等 10 秒后再同时上 A、B

第 4 步:加两个终端 ​

终端 C 放到路由器 A 旁边,终端 D 放到路由器 B 旁边,各自上电,等 30 秒。

检查项应该是
终端的 RoleEnd Device
终端的 Children0(正常——终端不能做父节点)
终端的 Neighbors通常只有 1~2 条
终端的归属从它父节点的 Children 列表里看

到这里网络就搭好了,可以正式采集、画图了。

串口准备与命令 ​

连接节点(板载 USB 转串口) ​

节点上已板载 USB 转串口芯片(CH340 / CP2102 / FT232 / PL2303 等),所以: ✅ 不需要外接 USB 转 TTL 模块 ✅ 不需要自己交叉接 TX / RX ✅ 不需要单独接 GND

直接用 USB 线把节点插到 PC,系统会识别出一个虚拟串口(COM 口)。

第一次使用必须先装驱动

驱动型号看板子上转串口芯片的丝印:

芯片驱动
CH340 / CH341CH341SER(沁恒)
CP2102 / CP2104Silicon Labs CP210x VCP Driver
FT232FTDI VCP Driver
PL2303Prolific PL2303 Driver

装好后在设备管理器 → 端口里能看到 USB-SERIAL CH340 (COMx) 之类的条目。

断电 = 拔掉该节点的 USB 线(板子由 USB 供电)。本实验里"给节点断电 / 重新上电"就是靠拔插 USB 线完成的,进阶观察环节要用到。

强烈建议用 USB Hub 把 5 块板同时插上,这样 5 个串口同时在线,不用来回换线。USB 2.0 普通线一般 3~5 m 就到极限;本实验节点都摆在 1~3 m 范围内,一般够用;若不够,把 USB Hub 放到中继点再拉短线。

分清哪个 COM 口是哪块板 ​

5 块板插上后会出现 5 个 COM 口,必须搞清对应关系,否则数据会记串。两种方法:

方法一(推荐,最快):全部插好后,逐个 COM 口发送 t,看报告开头的 Role 和 IEEEAddr,与自己贴的板子标签对上。 方法二:先只插一块板,记下新出现的 COM 号;再插下一块,如此类推。

串口助手参数 ​

参数值
端口号该节点对应的 COM 口
波特率115200(若乱码,改试 38400)
数据位 / 校验位 / 停止位8 / 无 / 1
流控无
显示模式字符(不要勾 HEX)

波特率到底是 115200 还是 38400? ​

先设 115200。 依据是固件的初始化顺序:

谁开的串口什么时候波特率
MT 监控层(MT_TaskInit → MT_UartInit)先(osalInitTasks 中靠前)38400
应用层(zclSampleSw_Init → zclSampleSw_InitUart)后115200

两次都打开 HAL_UART_PORT_0,而 TI 的 HalUARTOpenDMA() 里没有"只配置一次"的保护,所以后打开的应用层设置会覆盖掉前面的——实际生效的是 115200。

怎么判断选对了? 发一个字符(比如 x)看回显:

你看到的说明怎么做
干净地回显 x波特率正确继续做实验
收到一堆乱码(有数据但是乱的)波特率不对改成 38400 再试
完全没有任何数据不是波特率问题查 COM 口选错没有、驱动装没装、USB 线是不是只能充电

⚠️ 最后一行很重要:"完全没数据"和"有乱码"是两类完全不同的故障。 没数据 → 通路问题;有乱码 → 波特率问题。别在没数据的时候反复改波特率。

⚠️ 大多数串口助手(如 SSCOM)一个窗口只能打开一个串口。要同时观察多块板,就开多个程序窗口(或把程序复制几份),每个窗口选一个 COM 口。本实验最少只需盯 3 个 COM 口(协调器、路由器 A、路由器 B)。

学会读报告、用命令 ​

节点上电组网后,会在串口上每 10 秒自动打印一次自己的网络信息。但做实验时请用手动查询:在串口助手的发送框输入一个字符并点"发送"。

你输入节点会做什么
t立即打印一次拓扑报告 ← 本实验最常用
p自动打印 开 / 关
h 或 ?打印命令列表

强烈建议:一开始就对每个节点发一次 p,关掉自动打印,之后全靠 t 取数,否则串口一直刷屏,抄数据很痛苦。⚠️ 这些设置掉电不保存,节点断电重启后需要重新发 p。

一份实际报告的样子(路由器 A):

+-------- TOPO START 0x796F --------
Role      : Router
ShortAddr : 0x796F
IEEEAddr  : 00124B0001A2B3C5
PANID     : 0x1A2B
Channel   : 11
Depth     : 1
CoordAddr : 0x0000
Parent    : 0x0000
Children  : 1
   -> 0xABCD  Child/EndDev/rxOff  LQI=152 / 255
Neighbors : 3
   [0] 0x0000  IEEE=00124B0001A2B3C4  LQI=231 / 255
   [1] 0xABCD  IEEE=00124B0001A2B3C8  LQI=152 / 255
   [2] 0x1234  IEEE=00124B0001A2B3C6  LQI=167 / 255
+--------- TOPO END 0x796F ---------

画网状图时,你只需要看这 3 项:

字段画图时怎么用
ShortAddr / Depth标在节点旁边
Parent + Children决定实线箭头怎么画(子 → 父)
Neighbors 列表找出不是父子、但互相出现的那些 → 画虚线

提速技巧 ​

  1. 5 块板全部插在 USB Hub 上,任务里只拔插、不换线;建议开 3 个串口助手窗口,分别盯协调器、路由器 A、路由器 B。
  2. 最少只采 3 个节点:协调器 + 路由器 A + 路由器 B。因为父节点的 Children 字段里已经列出了它的全部子节点,只要采到 3 个有子节点的路由设备,整张图就完整了。
  3. 一开始就发 p 关掉自动打印,之后每次只发 t,串口干净、抄数快。
  4. 先摆好位置,再上电;要改布局,必须先给所有节点断电(拔 USB)再重新摆放和上电。
  5. 三人分工:一人读串口数据,一人记录,一人画图。边采集边画图。

拓扑图绘制规范(画图前先看) ​

图元表示
正方形协调器
圆形路由器
三角形终端
实线箭头(子 → 父)父子关系(看 Parent / Children)
虚线(不加箭头)邻居关系(看 Neighbors,且非父子)← 网状的关键
线上的数字该链路的 LQI
节点旁标注短地址(如 0x796F),括号里写 Depth

网状拓扑的示例画法:

                          ┌────────┐
                          │ 0x0000 │ 协调器 (D0)
                          └──┬──┬──┘
                    LQI=231  │  │  LQI=198
                    ┌────────┘  └────────┐
               ┌────▼───┐           ┌───▼────┐
               │ 0x796F │···········│ 0x1234 │
               └────┬───┘  LQI=167  └───┬────┘
         LQI=152   │        ↑           │  LQI=176
              ┌────▼───┐    │      ┌────▼───┐
              │ 0xABCD │    │      │ 0x5678 │
              └────────┘    │      └────────┘
              终端C(D2)     │      终端D(D1)
                          虚线:
                    A 与 B 互为邻居但不是父子
                   → 出现回路 → 冗余路径 → 网状

看图要点:

  • 实线永远构成一棵树(每个节点只有一个父节点);
  • 虚线是额外多出来的邻居链路,它打破了树结构、形成回路;
  • 有回路 = 有冗余 = 网状。

必做任务:搭建网状拓扑 ​

目标:让 3 个具备路由能力的设备(协调器、路由器 A、路由器 B)两两互为邻居,画出带冗余路径的网状拓扑图。

布置 ​

                    ┌────────┐
                    │ 协调器  │   0x0000  (D0)
                    └───┬────┘
            1~3 m   ╱       ╲   1~3 m
                  ╱           ╲
       ┌─────────▼──┐       ┌──▼─────────┐
       │  路由器 A   │·······│  路由器 B   │
       │            │ 1~3 m │            │
       └─────┬──────┘       └──────┬─────┘
             │ 0.5 m               │ 0.5 m
         ┌───▼───┐             ┌───▼───┐
         │ 终端 C │             │ 终端 D │
         └───────┘             └───────┘

步骤 ​

  1. 全部断电——把 5 块板的 USB 线全部拔掉;

  2. 按上图摆放:协调器、路由器 A、路由器 B 摆成一个三角形,两两距离 1~3 m(保证互相能听到);终端 C 贴近路由器 A(0.5 m 内),终端 D 贴近路由器 B;

  3. 按下面的顺序上电——这一步很关键:

    • 先只给协调器插 USB,等 10~15 秒(让它把网络建起来);
    • 再同时给路由器 A 和路由器 B 插 USB,等 20 秒;
    • 最后给终端 C 和终端 D 插 USB,等 30 秒。

    为什么必须按这个顺序? 如果 A、B、协调器同时上电,可能 A 抢先入网后就成了"父节点",B 随后挂到 A 下面——这样 A、B 变成父子关系,图上就没有冗余虚线了。先让协调器独自把网建起来,A、B 就会都直接挂在协调器下,这才是我们要的网状结构。

  4. 等 30 秒让网络稳定;

  5. 依次给协调器、路由器 A、路由器 B 发 p(关自动打印)和 t(取数);

  6. 重点核对:这 3 个设备的 Neighbors 里,是不是每一个都包含另外两个?(这就是"两两互为邻居")

  7. 记录数据、画图。

判据(画完对照检查) ​

检查项应该是
三个路由设备的邻居表两两互相出现:协调器↔A、协调器↔B、A↔B
协调器的 Children同时包含路由器 A 和路由器 B(说明它们都是协调器的直接子节点)
A、B 的 Parent都应该是 0x0000。如果有一个是对方的短地址,说明上电顺序没做好,要重来
父子关系仍然是一棵树:每个非协调器节点只有一个 Parent
图上的实线每个非协调器节点画一条实线箭头指向它的 Parent
图上的虚线路由器 A 与路由器 B 之间(它们互为邻居但不是父子)
整体形状出现回路(协调器—A—B—协调器)→ 这就是冗余路径,也就是网状
终端 C、DChildren 为 0;它们的归属从父节点的报告里读

写一句结论:A 和 B 为什么互为邻居却不是父子?这条虚线对网络可靠性有什么用?


进阶观察(选做,时间够就做):验证冗余确实有用 ​

  1. 记录当前状态:终端 C 挂在谁下面(看路由器 A 或协调器的 Children);
  2. 把路由器 A 的 USB 拔掉(断电),等 30~60 秒;
  3. 给协调器和路由器 B 发 t;
  4. 观察:终端 C 有没有改挂到路由器 B 或协调器下?Children 列表怎么变了?
  5. 把变化画在同一张图上(用另一种颜色标出消失的链路和新增的链路);
  6. 写一句结论:________________________________________________

这一步能亲眼看到"一条路断了,数据自己找另一条路"——这正是网状拓扑的价值所在。

在线仿真环节:printf + Terminal I/O 查看辅助信息 ​

与实验一~三一致,除了 PC 串口助手,在线仿真(连接仿真器、进入 C-SPY 调试会话)过程中,也可以把关键字段用 printf 输出到 IDE 界面的 Terminal I/O 窗口,适合单板调试时快速核对拓扑字段读取是否正确。

配置步骤(在线仿真) ​

  1. 接线:仿真器连接开发板与 PC(在线仿真必需);
  2. Project → Options → General Options → Library Configuration:Library 选择 Full(完整库,printf 才可用);
  3. 点击 Debug(Ctrl+D) 进入在线仿真调试会话(C-SPY 连接仿真器);
  4. 菜单 View → Terminal I/O 打开 IDE 界面上的 Terminal I/O 窗口;
  5. 全速运行程序,printf 辅助信息实时显示在该窗口。

可选:用 printf 打印关键字段 ​

在 Topo_Print() 里(或新增一个调试函数),把最关键的拓扑字段用 printf 输出:

c
#include <stdio.h>

void Topo_Print_ToTerminalIO(void)
{
  printf("[TOPO] Addr:0x%04X Depth:%d PAN:0x%04X CH:%d\r\n",
         _NIB.nwkDevAddress, _NIB.nodeDepth,
         _NIB.nwkPanId, _NIB.nwkLogicalChannel);
  printf("  Parent:0x%04X\r\n", NLME_GetCoordShortAddr());
}

注意

  • Library 必须为 Full,否则 printf 不可用;
  • Terminal I/O 只在**在线仿真调试会话(Debug)**中显示;
  • 本实验 5 块板同时观察时,仍以串口助手为主(多窗口并行),Terminal I/O 用于单板调试核对。

常见问题排查(运行期) ​

编译、烧写的问题请看「编译 / 烧写常见错误」。

现象处理
插上 USB 后找不到 COM 口① USB 转串口驱动没装或装错(看丝印,见前文);② 换一根 USB 线(有些线只能充电);③ 设备管理器看有没有黄色感叹号
设备管理器里出现未知设备 / 黄色感叹号驱动问题。卸载该设备后重新安装驱动,再重新插拔
串口完全无输出① 选错了 COM 口;② 波特率改试 38400;③ 该 COM 口被另一个窗口占用;④ 改动 3、改动 4 是否已做并重新烧写
提示"串口已被占用 / 打开失败"一个 COM 口同时只能被一个程序打开。检查是不是已经开了一个窗口连着这个口
输出是乱码波特率不对,115200 和 38400 都试一遍
插满 5 块板后有的板子掉线 / COM 口消失USB 供电不足。改用带独立电源的 USB Hub,或减少同时插的板子数量
拔插 USB 后 COM 口号变了Windows 有时会重新分配。重新到设备管理器确认一次;建议固定用同一个 USB 插口位置
一直显示 not joined还未入网。确认协调器已先上电;给该节点重新上电(拔插 USB);仍不行请找老师(可能需要擦除节点存储)
某块板等了 1 分钟以上仍不入网擦除该节点的 NV 再上电。旧网络参数会让它先尝试"重入旧网",而 Z-Stack 的重入退避最长可达 15 分钟(REJOIN_BACKOFF=900000 ms)
发字符没反应① 串口助手要点"发送"按钮;② 确认该节点已入网;③ 检查改动 5 是否已做并重新烧写
串口一直刷屏停不下来发一次 p 关掉自动打印
A 的邻居表里没有 B① 把 A、B 再靠近一点(1 m 以内);② 拔掉 USB 断电后重新上电;③ 检查是否真的把 A、B 都烧成了 RouterEB
Children : 0 但明明有子节点子节点刚入网,等 10 秒再发一次 t
邻居表条目比预期多正常。邻居表包含所有能听到的设备,不只是父子关系
终端的 Children 永远是 0正常现象,终端不能做父节点;终端的归属要从它父节点的报告里看
三个路由设备的 Parent 都不是 0x0000说明 A 或 B 挂到了对方下面(此时 A、B 变成父子关系,就不是网状了)。断电重来,先上协调器再上 A、B

思考题(选做 3 题,写在报告里) ​

  1. 为什么协调器的短地址一定是 0x0000?其他节点的短地址是谁分配的?
  2. 为什么终端设备的 Children 永远是 0?
  3. 你画的网状图里,路由器 A 和路由器 B 互为邻居,但它们的 Parent 都是协调器。为什么它们不互为父子?ZigBee 是靠哪个字段判断父子关系的?
  4. 代码题:Topo_Print() 里,父节点 / 子节点是从哪张表读出来的?邻居表又是用哪个函数读的?为什么这两者不能混为一谈?
  5. 代码题:Topo_RxByte() 为什么只记录字符、不直接打印?如果直接在 UART 中断里打印 400 字节的报告,可能会出什么问题?
  6. 图上那条虚线(A—B 互为邻居)对网络有什么实际好处?如果把它去掉,网络会变成什么结构?会有什么风险?
  7. 如果要把网络扩大到 20 个节点,网状特征会更明显还是更不明显?为什么?
  8. 星型、树型拓扑各有什么缺点,是网状能够弥补的?

实验报告要求 ​

交付物一:烧写成功的固件——提交修改后的 zcl_samplesw.c(标出你改的 5 处),报告里贴一张 IAR 编译成功的截图(0 error)。

交付物二:1 张手绘网状拓扑图,图上必须标注:

  • 节点短地址(如 0x796F)与深度(如 D1);
  • 实线箭头表示父子关系,虚线表示邻居关系;
  • 链路上的 LQI;
  • 并用文字指出:哪一条是形成冗余的虚线。

报告还应包含:

  1. 实验目的与原理简述(自己组织语言,重点说明关联表与邻居表的区别、以及网状拓扑为什么需要"虚线");
  2. 节点登记表(见下方附录);
  3. 采集数据;
  4. 图下写一句结论:你的网络在什么条件下形成了网状特征;
  5. 选做进阶观察的记录(如果做了);
  6. 思考题 3 题;
  7. 实验小结(改了哪些代码、遇到过什么编译/烧写错误、怎么解决的,2~3 句)。

报告首页格式:姓名 / 学号 / 班级 / 实验日期

附录 ​

附录 A:输出字段中英对照 ​

输出字段中文含义
TOPO START <addr>报告开始,<addr> 是这份报告所属节点的短地址
Role设备角色:Coordinator / Router / End Device
ShortAddr本机 16 位网络短地址
IEEEAddr本机 64 位 IEEE 地址
PANID网络标识符(同一网络的所有节点相同)
Channel射频信道(11~26,同一网络相同)
Depth网络深度 = 到协调器的跳数
CoordAddr协调器短地址(正常都是 0x0000)
Parent父节点短地址;协调器显示 (none)
Children子节点数量
-> 0x....一个子节点的短地址
Child/Router/rxOn子节点是路由器
Child/EndDev/rxOff子节点是终端(rxOff = 不常接收,睡眠终端)
Neighbors邻居表条目数
[n] 0x.... IEEE=... LQI=...第 n 个邻居的短地址、IEEE 地址、链路质量
LQI链路质量,0~255,越大越好
TOPO END <addr>报告结束
TOPO: not joined yet. Role=... devState=...该节点尚未入网,同时告诉你它认为自己是什么角色、以及内部状态值——见分级调试中的判断表

附录 B:串口终端命令速查 ​

输入作用
t立即打印一次拓扑报告(最常用)
p自动打印 开 / 关
+ / -自动打印周期 +5 秒 / −5 秒
h 或 ?打印命令列表和当前周期

附录 C:网状拓扑采集记录表 ​

表 1 节点登记表(只填一次)

节点物理标签COM 口IEEE 地址
协调器
路由器 A
路由器 B
终端 C
终端 D

表 2 网状拓扑采集记录表

布局说明:______________________

采集的节点ShortAddrDepthParentChildren(短地址 + LQI)Neighbors(短地址 + LQI)
协调器0—
路由器 A
路由器 B

两两互为邻居核对(有就在方框里打 √)

协调器路由器 A路由器 B
协调器—□□
路由器 A□—□
路由器 B□□—

A 与 B 那一格打上 √,就说明你的网络具备网状特征。

网状拓扑图(画在下面的方框里,或画在方格纸上后贴过来)

┌──────────────────────────────────────────────────────┐
│                                                      │
│                                                      │
│                                                      │
│                                                      │
│                                                      │
│                                                      │
│                                                      │
│                                                      │
└──────────────────────────────────────────────────────┘

结论(一句话):________________________________________________

进阶观察(选做):拔掉路由器 A 后,终端 C 的 Parent 从 ______ 变成了 ______,用了大约 ______ 秒恢复。

实验日期:____________  姓名:____________  学号:____________  班级:____________

关联知识 ​

aioteer · 物联网专业课程群

💬AI 伴学配置