实验四:Z-Stack 拓扑实验
一句话理解
ZigBee 网络的拓扑信息就藏在每个节点的 3 张表里:_NIB(我是谁)、关联表(谁是我的父/子)、邻居表(我能听到谁)。本实验用一个 topo.c 小模块把 3 张表读出来,分片从串口打印,再用打印结果手绘一张网状(Mesh)拓扑图——实线是父子、虚线是邻居,出现虚线回路就说明网络具备网状特征。
前置知识
本实验建立在两个前置之上:
- 2.3 ZigBee 3.0 组网实验:协调器建网、路由器/终端入网、角色配置(CoordinatorEB / RouterEB / EndDeviceEB);
- 实验一:Z-Stack 简单组网实验:入网成功后读取并打印 PAN ID / 短地址 / 父节点。
本实验的增量是:不只打印"我的父节点",而是把节点内部的三张表全部读出来,形成一份完整的"拓扑报告",供手工绘制网络拓扑图。
实验目的
- 学会在 IAR 中修改并编译一个 Z-Stack 工程,掌握固件下载流程;
- 理解 ZigBee 网络拓扑信息在固件内部是怎么存放和读取的(
_NIB、关联表AssociatedDevList[]、邻居表NeighborTable); - 认识协调器、路由器、终端三种设备类型的角色差异,理解**网络深度(Depth)**的含义;
- 理解网状(Mesh)拓扑的本质:路由器之间也存在链路,形成到协调器的冗余路径;
- 用串口终端采集各节点的拓扑报告,手工绘制网状拓扑图,区分父子关系(实线)与邻居关系(虚线)。
实验器材
| 器材 | 数量 | 说明 |
|---|---|---|
| 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 套 | 编译工程、观察串口 |
| 方格纸、铅笔、直尺 | 若干 | 拓扑图的产出靠它 |
实验原理
三种设备
关键:路由器和终端都能"入网",但只有路由器能当"二传手"——网状拓扑正是靠路由器互连形成的,本实验要用的正是这一点。
网络深度 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.eww | IAR 工作区,用 IAR 打开它 |
如果老师只发给你
topo.c/topo.h两个文件,先把它们复制到Source\目录下。
topo 模块在干什么(动手改代码前务必先看)
Topo_Print() 只做一件事:读 3 张表,拼成文本,从串口打出去。
| 报告字段 | 代码里从哪里读 |
|---|---|
| ShortAddr / PANID / Channel / Depth / CoordAddr | _NIB.nwkDevAddress / nwkPanId / nwkLogicalChannel / nodeDepth / nwkCoordAddress |
| IEEEAddr | NLME_GetExtAddr() |
| Role | 编译宏 ZDO_COORDINATOR / RTR_NWK(由链接配置决定) |
| Parent / Children | 遍历 AssociatedDevList[],用 AssocFindDevice(i) 取每一项,看它的 nodeRelation |
| Neighbors + LQI | NLME_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 加入工程
- 用 IAR 打开
CC2530DB\SampleSwitch.eww; - 左上角配置下拉框选 CoordinatorEB;
- 在左侧工程树里找到 SampleSwitch – Source 分组;
- 右键该分组 → Add → Add Files…,选中
Source\topo.c,确定(topo.h一般自动被索引,若 IAR 要求手动加头文件,一并加入); - IAR 的
.ewp只有一份文件列表——只需添加一次,三个配置都会编译它。
步骤二:修改 zcl_samplesw.c(5 处)
打开 Source\zcl_samplesw.c,按下面 5 处修改。行号可能略有偏差,用搜索关键字定位更可靠。 ⚠️ 不要在这个文件里输入中文(IAR 对 UTF-8 支持不佳),注释用英文。
改动 1 —— 加头文件
找到:
#include "zcl_samplesw.h"改为:
#include "zcl_samplesw.h"
#include "topo.h"改动 2 —— 加拓扑模块的事件宏
找到:
#define SAMPLESW_TOGGLE_TEST_EVT 0x1000改为:
#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() 放在了一个条件编译块里:
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 | ... );
#endifZDO_COORDINATOR 只在协调器固件里定义(见配置 f8wCoord.cfg),所以在路由器(RouterEB)和终端(EndDeviceEB)里这一整块都被编译掉了——串口从来没被初始化过,既没有回声、也没有任何输出。
编译时会看到这条警告,它就是在提示这个问题:
Warning[Pe177]: function "zclSampleSw_InitUart" was declared but never referenced看到 Pe177 就是没改对,路由器一定没有串口输出。
找到这一整块(大约在 zclSampleSw_Init() 的中后段):
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 保持原位不动):
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() 里最后一个事件分支的结尾与函数返回:
if ( events & SAMPLEAPP_REJOIN_EVT )
{
...
return ( events ^ SAMPLEAPP_REJOIN_EVT );
}
// Discard unknown events
return 0;在两者之间插入:
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():
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);
}
}改为:
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、B | CC2530DB\RouterEB\Exe\SampleSwitch.hex |
| EndDeviceEB | 终端 C、D | CC2530DB\EndDeviceEB\Exe\SampleSwitch.hex |
每个配置的操作:下拉框选中配置 → Project → Rebuild All → 应 0 error 通过。
⚠️
CC2530DB工程里一共有 5 个配置,别选错:CoordinatorEB/RouterEB/EndDeviceEB/RouterEB - OTAClient/EndDeviceEB - OTAClient。本实验只用前三个——后两个带 OTA 空中升级功能,内存占用更大。
省时间技巧:同组分工每人只编一个配置,把 Exe 目录下的 .hex 放到共享文件夹互相取用;第二次编译是增量编译,快很多。
步骤四:烧写五块板
| 节点 | 角色 | 烧写文件 |
|---|---|---|
| 协调器 | Coordinator | CoordinatorEB\Exe\SampleSwitch.hex |
| 路由器 A | Router | RouterEB\Exe\SampleSwitch.hex |
| 路由器 B | Router | 同上 |
| 终端 C | End Device | EndDeviceEB\Exe\SampleSwitch.hex |
| 终端 D | End 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 definition | CC2530 的 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"按下面顺序排查:
- 确认用的是最新版(v3)的
topo.c/topo.h。最新版把整个模块的 XDATA 占用压到了约 22 字节:输出缓冲只有 16 字节且装满就立刻发出去、不使用占 XDATA 的十六进制查表、只保留一个需要初始化的变量。旧版会多占约 100 字节——正是这个报错的来源。 - 确认编译配置没选错。只用
CoordinatorEB/RouterEB/EndDeviceEB,不要选RouterEB - OTAClient/EndDeviceEB - OTAClient,那两个多带了 OTA 功能,内存占用更大。 - 把输出缓冲再调小(最后的手段):打开
topo.h,把TOPO_LINE_BUF_LEN由16改成8。因为缓冲是"装满就发",调小不会截断任何内容,只是 UART 的调用次数略多。 - 如果以上都不行,说明工程本身已接近内存上限,请联系老师确认工程配置,或关闭本实验用不到的可选功能(如 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 |
| 2 | zclSampleSw_Init() 里的 osal_start_timerEx(...),且在 #ifdef ZDO_COORDINATOR 外面 | 改动 3 |
| 3 | zclSampleSw_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(),看它的源码:
if ((len + dmaCfg.txIdx[dmaCfg.txSel]) > HAL_UART_DMA_TX_MAX)
{
return 0; /* ← 缓冲满了就返回 0,数据被静默丢弃!不报错、不等待 */
}三个关键点串起来就是事故:
| # | 事实 |
|---|---|
| 1 | DMA 发送缓冲只有 约 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。
| 现象 | 说明 | 下一步 |
|---|---|---|
看到回显 x | UART 收发、波特率、驱动全都正常 | 问题在拓扑模块 → 查改动 3 / 改动 4 |
| 什么都看不到 | UART 通路本身有问题 | 波特率试 38400;换 COM 口;查驱动和 USB 线 |
为什么要先做回声测试? 回声是工程原有的功能(
zclSampleSw_UartCB()里那行HalUARTWrite),和我们改的代码无关。所以它能干净地把"串口本身的毛病"和"我们代码的毛病"分开。这是最省时间的一步——否则你会去翻代码,其实只是波特率选错了。
(3)命令测试:
| 发什么 | 应该看到什么 |
|---|---|
h | 命令列表 + 当前周期 |
t | 一份完整报告 |
p | TOPO: automatic report interval = OFF |
只要 h 能出来,就说明改动 1、2、3、4 全对了。
(4)核对内容——单协调器时期的期望值:
| 字段 | 单独一个协调器时应该是 | 如果不对 |
|---|---|---|
Role | Coordinator | 烧错配置了 |
ShortAddr | 0x0000 | 同上 |
IEEEAddr | 16 位十六进制 | 抄下来,这是这块板的唯一身份 |
PANID | 一个随机值(不是 0xFFFF) | 建网未成功 |
Channel | 11 | 工程默认信道 |
Depth | 0 | |
CoordAddr | 0x0000 | |
Parent | (none) | 协调器没有父节点,正确 |
Children | 0 | 现在还没有节点入网,正确 |
Neighbors | 0(列表为空) | 正确 |
注意:报告是分几批刷出来的,不是一次打完。 为了让串口 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):
| 值 | 名称 | 含义 |
|---|---|---|
| 0 | DEV_HOLD | 已初始化但未启动 |
| 1 | DEV_INIT | 已初始化,未连网 |
| 2 | DEV_NWK_DISC | 正在扫描可加入的网络 ← 路由器/终端刚上电时是这个 |
| 3 | DEV_NWK_JOINING | 正在加入网络 |
| 5 | DEV_END_DEVICE_UNAUTH | 已加入,等信任中心认证 |
| 6 | DEV_END_DEVICE | 终端已正常工作 |
| 7 | DEV_ROUTER | 路由器已正常工作 |
| 8 | DEV_COORD_STARTING | 协调器正在建网 |
| 9 | DEV_ZB_COORD | 协调器已建网 |
| 10 | DEV_NWK_ORPHAN | 与父节点失联 |
| 11 | DEV_NWK_KA | 正在给父节点发保活 |
| 12 | DEV_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);- 顺便也验证了串口、命令、分片发送都正常。
- 给路由器烧
RouterEB,单独上电,等 20~30 秒; - 给协调器发
t→Children应该从0变成1,并列出路由器的短地址; - 给路由器发
t→ 应该看到Role: Router、Depth: 1、Parent: 0x0000; - 交叉验证:协调器
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 个路由器,验证"两两互为邻居"(本实验的成败判据)
- 保持协调器已建网;
- 把路由器 A、B 放在 1~3 m 内,与协调器构成三角形;
- 分别给 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 秒。
| 检查项 | 应该是 |
|---|---|
终端的 Role | End Device |
终端的 Children | 0(正常——终端不能做父节点) |
终端的 Neighbors | 通常只有 1~2 条 |
| 终端的归属 | 从它父节点的 Children 列表里看 |
到这里网络就搭好了,可以正式采集、画图了。
串口准备与命令
连接节点(板载 USB 转串口)
节点上已板载 USB 转串口芯片(CH340 / CP2102 / FT232 / PL2303 等),所以: ✅ 不需要外接 USB 转 TTL 模块 ✅ 不需要自己交叉接 TX / RX ✅ 不需要单独接 GND
直接用 USB 线把节点插到 PC,系统会识别出一个虚拟串口(COM 口)。
第一次使用必须先装驱动
驱动型号看板子上转串口芯片的丝印:
| 芯片 | 驱动 |
|---|---|
| CH340 / CH341 | CH341SER(沁恒) |
| CP2102 / CP2104 | Silicon Labs CP210x VCP Driver |
| FT232 | FTDI VCP Driver |
| PL2303 | Prolific 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 列表 | 找出不是父子、但互相出现的那些 → 画虚线 |
提速技巧
- 5 块板全部插在 USB Hub 上,任务里只拔插、不换线;建议开 3 个串口助手窗口,分别盯协调器、路由器 A、路由器 B。
- 最少只采 3 个节点:
协调器+路由器 A+路由器 B。因为父节点的Children字段里已经列出了它的全部子节点,只要采到 3 个有子节点的路由设备,整张图就完整了。 - 一开始就发
p关掉自动打印,之后每次只发t,串口干净、抄数快。 - 先摆好位置,再上电;要改布局,必须先给所有节点断电(拔 USB)再重新摆放和上电。
- 三人分工:一人读串口数据,一人记录,一人画图。边采集边画图。
拓扑图绘制规范(画图前先看)
| 图元 | 表示 |
|---|---|
| 正方形 | 协调器 |
| 圆形 | 路由器 |
| 三角形 | 终端 |
| 实线箭头(子 → 父) | 父子关系(看 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 │
└───────┘ └───────┘步骤
全部断电——把 5 块板的 USB 线全部拔掉;
按上图摆放:协调器、路由器 A、路由器 B 摆成一个三角形,两两距离 1~3 m(保证互相能听到);终端 C 贴近路由器 A(0.5 m 内),终端 D 贴近路由器 B;
按下面的顺序上电——这一步很关键:
- 先只给协调器插 USB,等 10~15 秒(让它把网络建起来);
- 再同时给路由器 A 和路由器 B 插 USB,等 20 秒;
- 最后给终端 C 和终端 D 插 USB,等 30 秒。
为什么必须按这个顺序? 如果 A、B、协调器同时上电,可能 A 抢先入网后就成了"父节点",B 随后挂到 A 下面——这样 A、B 变成父子关系,图上就没有冗余虚线了。先让协调器独自把网建起来,A、B 就会都直接挂在协调器下,这才是我们要的网状结构。
等 30 秒让网络稳定;
依次给协调器、路由器 A、路由器 B 发
p(关自动打印)和t(取数);重点核对:这 3 个设备的
Neighbors里,是不是每一个都包含另外两个?(这就是"两两互为邻居")记录数据、画图。
判据(画完对照检查)
| 检查项 | 应该是 |
|---|---|
| 三个路由设备的邻居表 | 两两互相出现:协调器↔A、协调器↔B、A↔B |
协调器的 Children | 同时包含路由器 A 和路由器 B(说明它们都是协调器的直接子节点) |
A、B 的 Parent | 都应该是 0x0000。如果有一个是对方的短地址,说明上电顺序没做好,要重来 |
| 父子关系 | 仍然是一棵树:每个非协调器节点只有一个 Parent |
| 图上的实线 | 每个非协调器节点画一条实线箭头指向它的 Parent |
| 图上的虚线 | 路由器 A 与路由器 B 之间(它们互为邻居但不是父子) |
| 整体形状 | 出现回路(协调器—A—B—协调器)→ 这就是冗余路径,也就是网状 |
| 终端 C、D | Children 为 0;它们的归属从父节点的报告里读 |
写一句结论:A 和 B 为什么互为邻居却不是父子?这条虚线对网络可靠性有什么用?
进阶观察(选做,时间够就做):验证冗余确实有用
- 记录当前状态:终端 C 挂在谁下面(看路由器 A 或协调器的
Children); - 把路由器 A 的 USB 拔掉(断电),等 30~60 秒;
- 给协调器和路由器 B 发
t; - 观察:终端 C 有没有改挂到路由器 B 或协调器下?
Children列表怎么变了? - 把变化画在同一张图上(用另一种颜色标出消失的链路和新增的链路);
- 写一句结论:________________________________________________
这一步能亲眼看到"一条路断了,数据自己找另一条路"——这正是网状拓扑的价值所在。
在线仿真环节:printf + Terminal I/O 查看辅助信息
与实验一~三一致,除了 PC 串口助手,在线仿真(连接仿真器、进入 C-SPY 调试会话)过程中,也可以把关键字段用 printf 输出到 IDE 界面的 Terminal I/O 窗口,适合单板调试时快速核对拓扑字段读取是否正确。
配置步骤(在线仿真)
- 接线:仿真器连接开发板与 PC(在线仿真必需);
- Project → Options → General Options → Library Configuration:Library 选择 Full(完整库,printf 才可用);
- 点击 Debug(Ctrl+D) 进入在线仿真调试会话(C-SPY 连接仿真器);
- 菜单 View → Terminal I/O 打开 IDE 界面上的 Terminal I/O 窗口;
- 全速运行程序,printf 辅助信息实时显示在该窗口。
可选:用 printf 打印关键字段
在 Topo_Print() 里(或新增一个调试函数),把最关键的拓扑字段用 printf 输出:
#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 题,写在报告里)
- 为什么协调器的短地址一定是
0x0000?其他节点的短地址是谁分配的? - 为什么终端设备的
Children永远是 0? - 你画的网状图里,路由器 A 和路由器 B 互为邻居,但它们的
Parent都是协调器。为什么它们不互为父子?ZigBee 是靠哪个字段判断父子关系的? - 代码题:
Topo_Print()里,父节点 / 子节点是从哪张表读出来的?邻居表又是用哪个函数读的?为什么这两者不能混为一谈? - 代码题:
Topo_RxByte()为什么只记录字符、不直接打印?如果直接在 UART 中断里打印 400 字节的报告,可能会出什么问题? - 图上那条虚线(A—B 互为邻居)对网络有什么实际好处?如果把它去掉,网络会变成什么结构?会有什么风险?
- 如果要把网络扩大到 20 个节点,网状特征会更明显还是更不明显?为什么?
- 星型、树型拓扑各有什么缺点,是网状能够弥补的?
实验报告要求
交付物一:烧写成功的固件——提交修改后的 zcl_samplesw.c(标出你改的 5 处),报告里贴一张 IAR 编译成功的截图(0 error)。
交付物二:1 张手绘网状拓扑图,图上必须标注:
- 节点短地址(如
0x796F)与深度(如D1); - 实线箭头表示父子关系,虚线表示邻居关系;
- 链路上的 LQI;
- 并用文字指出:哪一条是形成冗余的虚线。
报告还应包含:
- 实验目的与原理简述(自己组织语言,重点说明关联表与邻居表的区别、以及网状拓扑为什么需要"虚线");
- 节点登记表(见下方附录);
- 采集数据;
- 图下写一句结论:你的网络在什么条件下形成了网状特征;
- 选做进阶观察的记录(如果做了);
- 思考题 3 题;
- 实验小结(改了哪些代码、遇到过什么编译/烧写错误、怎么解决的,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 网状拓扑采集记录表
布局说明:______________________
| 采集的节点 | ShortAddr | Depth | Parent | Children(短地址 + LQI) | Neighbors(短地址 + LQI) |
|---|---|---|---|---|---|
| 协调器 | 0 | — | |||
| 路由器 A | |||||
| 路由器 B |
两两互为邻居核对(有就在方框里打 √)
| 协调器 | 路由器 A | 路由器 B | |
|---|---|---|---|
| 协调器 | — | □ | □ |
| 路由器 A | □ | — | □ |
| 路由器 B | □ | □ | — |
A 与 B 那一格打上 √,就说明你的网络具备网状特征。
网状拓扑图(画在下面的方框里,或画在方格纸上后贴过来)
┌──────────────────────────────────────────────────────┐
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────┘结论(一句话):________________________________________________
进阶观察(选做):拔掉路由器 A 后,终端 C 的 Parent 从 ______ 变成了 ______,用了大约 ______ 秒恢复。
实验日期:____________ 姓名:____________ 学号:____________ 班级:____________