实验5、传感器数据收发(综合实验箱版本)
一句话理解
本实验以 AM2321 温湿度传感器为例,走通「51 单片机采集 → 串口帧 → CC2530 节点上报」全链路:51 负责读懂传感器并装成 Modbus 帧,CC2530 负责收帧、换算成 ZCL 单位并无线上报——把"传感器读取"和"无线收发"拆成两个可独立调试的模块。
副标题:以 AM2321 温湿度传感器为例,走通「51 单片机采集 → 串口帧 → CC2530 节点上报」全链路
适用:物联网综合实践 / 传感器与传感网 / 单片机与接口技术 课时建议:4 学时(2 学时硬件与 51 侧 + 2 学时 CC2530 侧与联调) 本文范围:只到 CC2530 节点上报为止,不含 PC 上位机。PC 端解析见《协调器-PC-二进制协议.md》。
0. 实验目标与准备
0.1 学完你应该能做到
| 编号 | 能力目标 | 验收方式 |
|---|---|---|
| G1 | 独立读出 AM2321 的温湿度,并正确解释 40 bit 里的符号位与校验和 | 检查点 1:串口助手看到正确的温度/湿度 |
| G2 | 用 printf 把变量打印到串口,并说明它为什么会"污染"协议帧 | 检查点 2:对比开/关调试打印时 CC2530 侧的现象差异 |
| G3 | 手工构造一帧 Modbus 读指令,用串口助手与 51 从机完成一次问答 | 检查点 3:手发 6 字节,收到 9 字节正确响应 |
| G4 | 在 CC2530 上完成 UART0 收发、帧同步与解析,并换算成 ZCL 单位 | 检查点 4:Terminal I/O 每 0.5 s 打印一行温湿度 |
| G5 | 区分「仿真终端打印」与「硬件串口通信」两条调试通道,各自用对场合 | 检查点 5:故意写错的 printf 格式符,观察实际输出 |
0.2 器材清单
| 器材 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|
| 温湿度采集板 | STC12C5A60S2 + AM2321 | 1 | 板上 AM2321 已接 P1.1 |
| ZigBee 节点板 | CC2530 模块 | 1 | 板载 UART0:P0_2 = RX,P0_3 = TX |
| 协调器 | CC2530 模块 + USB 转串口 | 1 | 本实验只用来确认节点入网 |
| 下载器 | CC Debugger | 1 | 配 IAR EW8051 8.0 |
| USB-TTL | CH340 / CP2102 | 1 | 调 51 侧时用,3.3 V 电平档 |
| 杜邦线 | 母-母 | 4 根 | 至少 3 根要能交叉接 |
0.3 前置知识自检
开始前请确认你能回答:
- UART 一帧 10 bit 里,起始位、数据位、停止位各在哪?
- 波特率 9600 时,传 1 个字节要多久?(答案:约 1.04 ms)
- "交叉接线"指 TX 接对方的什么?(答案:RX)
- 16 位有符号数
0xFFCB等于十进制的多少?(答案:−53)
答不出第 2、4 题的,先回看串口通信与补码两节。
1. 原理:这份数据为什么要"搬两次"
1.1 全链路数据流
┌──────────┐ 单总线(1根线) ┌──────────┐ UART 9600 8N1 ┌──────────┐ 无线 802.15.4 ┌────────┐
│ AM2321 │ ─────────────→ │ 51单片机 │ ──────────────→ │ CC2530 │ ─────────────→ │ 协调器 │
│ 温湿度 │ 40 bit 原始 │ STC12 │ 9 字节 Modbus │ 节点 │ ZCL 属性上报 │ │
└──────────┘ └──────────┘ └──────────┘ └────────┘
P1.1 P3.1 → P0_2 簇 0x0402 / 0x0405
P3.0 ← P0_3三段的"数据形态"完全不同,这是本实验最容易出错的地方:
| 段 | 数据形态 | 单位 | 谁负责转换 |
|---|---|---|---|
| ① AM2321 → 51 | 5 字节原始值(40 bit) | 0.1 ℃ / 0.1 %RH | 51:拆符号位、算校验和 |
| ② 51 → CC2530 | 9 字节 Modbus 帧 | 0.1 ℃ / 0.1 %RH | 51:原样装帧,不做缩放 |
| ③ CC2530 → 协调器 | ZCL 属性 | 0.01 ℃ / 0.01 %RH | CC2530:×10 |
为什么第 ② 段不直接放大 10 倍? 因为 AM2321 的物理精度就是 0.1,放大只会让低位的 0 看起来很"精密",属于虚假精度。 而 ZCL 标准强制规定
MeasuredValue用 0.01 为单位,所以只能在 ③ 段做一次转换。
1.2 为什么中间要插一块 51
学生常问:"CC2530 也有 ADC,为什么不让它直接读传感器?"
| 原因 | 说明 |
|---|---|
| 电压不同 | CC2530 的 I/O 是 3.3 V,AM2321 模块多为 5 V 供电(SDA 上拉到 5 V 会直接打坏 CC2530 的引脚);51 板是 5 V 系统,天然匹配 |
| 单总线对时序敏感 | AM2321 靠几十 µs 级的电平宽度区分 0/1,而 CC2530 跑在 Z-Stack 上会被协议栈和射频中断频繁打断,时序不可靠 |
| 引脚与资源 | CC2530 的 P0_2/P0_3 要留给 UART0;若再由它直读传感器,等于让同一个 MCU 同时承担"硬实时时序"和"协议栈"两件事 |
| 工程习惯 | 采集归采集、通信归通信;51 挂了不影响组网,故障可以分段定位 |
这也是本实验的教学核心:把"传感器读取"和"无线收发"拆成两个可独立调试的模块。
2. 第 1 步:在 51 上把温湿度读出来
2.1 硬件连接
| AM2321 | 51 单片机 | 说明 |
|---|---|---|
| SDA(数据) | P1.1 | 单总线,需 4.7 kΩ 上拉到 VCC |
| VCC | 5 V | |
| GND | GND |
⚠️ 不是 I²C。AM2321 只有一根数据线,没有 SCL。看到"两线制"就以为是 I²C,是本节最常见的概念错误。它用的是类似 DHT11 的单总线自定义时序。
2.2 工程位置
实验五综合实验箱/STC单片机读取温湿度传感器程序/
├── AM2321.uvproj ← Keil C51 工程
├── src/main.c ← 全部逻辑(GBK 编码)
└── out/AM2321.hex ← 编译产物,STC-ISP 烧这个用 Keil 打开 AM2321.uvproj,确认:
- 晶振 11.0592 MHz(Options → Target → Xtal)
- 器件选 STC12C5A60S2(若下拉框没有,选 AT89C52 也能编,但下载时以 STC-ISP 识别为准)
2.3 一次完整读取有 5 个阶段
阅读 Read_Sensor(),对照下面表格逐段理解:
| 阶段 | 代码动作 | 时间要求 | 判据 |
|---|---|---|---|
| ① 起始 | Sensor_SDA=0 → 保持 → 释放为 1 | 拉低 ≥ 3 ms | — |
| ② 等应答 | 释放后 30 µs 内模块把总线拉低 | 30 µs | Sensor_SDA != 0 → 模块没接上,直接 return 0 |
| ③ 应答脉宽 | 先低 80 µs,再高 80 µs | 约 160 µs | 超时 300 次计数即放弃 |
| ④ 收 40 bit | 循环 5 次 Read_SensorData() | 每位 76~120 µs | 见下方"0 和 1 怎么区分" |
| ⑤ 校验 | 前 4 字节之和的低 8 位 == 第 5 字节 | — | 不等则丢弃整帧,保留上次有效值 |
0 和 1 怎么区分:每 bit 都以 50 µs 低电平开头,之后高电平的宽度决定值——
高电平持续 26~28 µs → 该位 = 0
高电平持续 70 µs → 该位 = 1代码里的实现是:低电平结束后 delay_30us(),30 µs 这一刻采样——此时 0 已经变低、1 还是高,一刀切开:
delay_30us(); /* 卡在 30µs 采样点 */
tmp = 0;
if (Sensor_SDA) /* 还是高 → 1 */
{
tmp = 1;
}2.4 五个字节怎么变成温度湿度
Sensor_Data[0] 湿度高字节
Sensor_Data[1] 湿度低字节
Sensor_Data[2] 温度高字节 ← bit15 是符号位!
Sensor_Data[3] 温度低字节
Sensor_Data[4] 校验和湿度(无符号,直接拼):
TH_Humi10 = (Sensor_Data[0] << 8) | Sensor_Data[1]; /* 0.1 %RH */温度(有符号,必须先拆符号位):
if (Sensor_Data[2] & 0x80) /* bit15 = 1 → 负数 */
{
TH_Temp10 = -(int)(((Sensor_Data[2] & 0x7F) << 8) | Sensor_Data[3]);
}
else
{
TH_Temp10 = (int)((Sensor_Data[2] << 8) | Sensor_Data[3]);
}坑点:直接把
Sensor_Data[2]<<8 | Sensor_Data[3]强转int会得到一个巨大的正数(例如 −5.3 ℃ 会变成 65483), 因为 bit15 只在 16 位有符号数里才表示负号,unsigned char提升成int时它还在第 15 位,但数值语义已经错了。 必须手动清掉 bit15 再取反。
2.5 校验和:为什么必须做
Sensor_Data[0] + [1] + [2] + [3],取低 8 位,应等于 Sensor_Data[4]。
不做校验的后果很隐蔽:单总线在杜邦线较长、上拉偏弱时,会偶尔把某一位读错。温度可能从 25.3 ℃ 突然跳到 89.1 ℃,而程序毫不知情。校验失败的帧必须整帧丢弃,然后继续用上一次的有效值——这就是 TH_Valid 标志的作用。
2.6 采样周期为什么是 2 秒
AM2321 手册明确要求:两次读取间隔 ≥ 2 s,否则会返回上一次的陈旧数据甚至错误数据。
#define TH_READ_PERIOD 20 /* 20 × 100 ms = 2 s */
#define TH_LOOP_MS 100主循环里用 read_tick 计数实现,不用定时器中断——因为单总线时序对中断极其敏感,读传感器期间被打断就会崩。
✅ 检查点 1
打开 Keil 的 Watch 窗口,全速运行,观察:
| 观察项 | 期望 |
|---|---|
TH_Temp10 | 与环境温度一致(如 253 表示 25.3 ℃) |
TH_Humi10 | 与环境湿度一致(如 614 表示 61.4 %RH) |
TH_Valid | 稳定为 1 |
Read_Sensor() 返回值 | 连续多次为 1 |
对着传感器哈一口气,TH_Humi10 应该在 2~3 秒内明显上升。看不到变化就说明采样没跑起来,别急着接 CC2530。
3. 第 2 步:用 printf 调试 51(调试手段一)
3.1 为什么 printf 默认"看不见"
Keil C51 的 printf 并不直接操作硬件。它把每个字符交给 putchar(),而库里的 putchar 默认是个空实现(因为没有标准输出设备)。
所以只要自己写一个 putchar,把字符塞进串口就行——这叫重定向:
#include <stdio.h>
/* 覆盖库函数:printf 每输出一个字符,都会走到这里 */
char putchar(char c)
{
B_TI = 0;
SBUF = c;
while (!B_TI); /* 等中断里把 B_TI 置 1,说明发完了 */
B_TI = 0;
return c;
}3.2 一个必须注意的细节:别和 Uart1_TxByte 抢标志位
本工程发送用的是自定义标志 B_TI(在 UART1_RCV 中断里 TI=0; B_TI=1;),而不是标准的 while(!TI)。
如果 putchar 里写 while(!TI); TI=0;,会和中断里的 TI=0 互相踩,表现为偶尔丢字符或卡死。所以上面的 putchar 直接复用 B_TI。
更省事的写法是直接调用现成函数:
char putchar(char c)
{
Uart1_TxByte((unsigned char)c);
return c;
}3.3 加一个可以一键关掉的调试开关
直接往协议帧里插 printf 会出大问题,所以必须用宏隔离:
/* ===== 调试打印开关 =====
* 1 = 打开:往串口打印人读文本(此时不能接 CC2530!)
* 0 = 关闭:串口只跑 9 字节协议帧 */
#define TH_DEBUG_PRINTF 1在 main() 的采样分支里这样用(用 #if 包住,不用可变参数宏,任何版本的 C51 都能编):
if (++read_tick >= TH_READ_PERIOD)
{
read_tick = 0;
if (Read_Sensor())
{
#if TH_DEBUG_PRINTF
printf("T=%d.%d C H=%d.%d %%RH raw=%02X %02X %02X %02X %02X\r\n",
TH_Temp10 / 10, abs(TH_Temp10 % 10),
TH_Humi10 / 10, TH_Humi10 % 10,
Sensor_Data[0], Sensor_Data[1],
Sensor_Data[2], Sensor_Data[3], Sensor_Data[4]);
#endif
}
else
{
#if TH_DEBUG_PRINTF
printf("sensor read FAIL\r\n");
#endif
}
}
#if 0的代码在预处理阶段就被删掉了,连一个字节都不占,比运行时判断更干净。 而printf本身会拉进整个格式化库(约 +1 KB code),所以正式固件里一定要关。
注意
abs(TH_Temp10 % 10):C 语言里负数取模结果是负的(−53 % 10 = −3), 直接打印会输出T=-5.-3。加abs()才是T=-5.3。 需要#include <stdlib.h>。
3.4 操作步骤
| 步 | 操作 | 要点 |
|---|---|---|
| ① | 拔掉连到 CC2530 的 P3.0 / P3.1 两根线 | 否则 CC2530 和 USB-TTL 抢同一根线 |
| ② | USB-TTL 接 51 板:TTL 的 RX → P3.1(TXD),TX → P3.0(RXD),GND 共地 | 仍是交叉接 |
| ③ | Keil 里 TH_DEBUG_PRINTF 改成 1,Rebuild(全量重编) | 改宏后增量编译会漏文件 |
| ④ | STC-ISP 打开 out/AM2321.hex,选对 COM 口 → 下载/编程 → 冷启动上电 | STC 需要断电再上电才进下载 |
| ⑤ | 打开串口助手:9600、8N1、ASCII 显示 | 与 51 的 BRT 配置对应 |
3.5 预期输出
T=25.3 C H=61.4 %RH raw=02 66 00 FD 65
T=25.3 C H=61.4 %RH raw=02 66 00 FD 65
T=25.4 C H=61.5 %RH raw=02 67 00 FE 67逐字段核对:
| 字段 | 值 | 怎么验算 |
|---|---|---|
raw 前两字节 02 66 | 0x0266 = 614 | ÷10 = 61.4 %RH ✓ |
raw 后两字节 00 FD | 0x00FD = 253 | ÷10 = 25.3 ℃ ✓ |
raw 末字节 65 | 0x65 = 101 | 0x02+0x66+0x00+0xFD = 0x165,低 8 位 = 0x65 ✓ |
用你自己屏幕上的值验算,不要抄示例。 你房间的温湿度和这里不同,
raw五个字节自然也不同。 验算公式:(raw[0] + raw[1] + raw[2] + raw[3]) & 0xFF == raw[4]这条等式不成立就说明读到的数据是坏的(干扰、上拉不足、时序偏差),此时后面的实验全都不必做。
3.6 ⚠️ 关键警告:printf 和协议帧是同一个串口
这是本实验最重要的一个认知:
51 的 UART0(P3.0/P3.1)只有一条通道
│
├── printf 输出的人读文本 ──┐
│ ├──→ 同一根 TX 线 → CC2530
└── TH_SendFrame 的 9 字节 ──┘如果两者同时打开,CC2530 收到的字节流会变成:
54 3D 32 35 2E 33 20 43 ... 01 03 04 00 FD 02 66 EA 89 54 3D ...
└────── "T=25.3 C ..." ────┘ └────── 真正的帧 ──────┘ └─ 下一行文本CC2530 的帧同步逻辑是"遇到 0x01 才开始收"。文本里的 0x01 极少出现,所以它会一直把 ASCII 当成垃圾丢掉; 但一旦文本里恰好出现一个 0x01(例如打印的 raw 值里有 01),它就会从错误的位置开始拼帧,拼出一个 CRC 错误的假帧。
因此工程里有一条铁律:
原厂例程上电第一件事就是发一句欢迎字符串——这一句会先于所有数据帧发出去, CC2530 从它里面捡到 0x01 的概率不高,但前几帧的帧头必然错位,表现为"上电后前几秒数据乱跳"。 本工程把同样的欢迎语收进了一个开关,默认关掉,并在注释里写明原因:
#define TH_UART_BANNER 0 /* 1 = 上电发 "TH board ready"
* 【默认 0,必须保持 0】 */调试口诀: 接 USB-TTL 看 printf 时 → 拔掉 CC2530;接 CC2530 跑协议时 →
TH_DEBUG_PRINTF改 0。
✅ 检查点 2
- 把
TH_DEBUG_PRINTF改成 0,重新编译下载,串口助手切 HEX 显示。 - 此时串口上不该有任何主动输出——一片空白才是对的。
- 用串口助手发
01 03 00 2A 00 00(HEX 模式),应立刻收到 9 字节。 - 回答:如果不改 0 就接上 CC2530,会在 CC2530 的 Terminal I/O 里看到什么现象? (提示:
[51 TH] NO FRAME tx=N rxB=45 last=0x54,0x54就是字符'T')
4. 第 3 步:把"读数"变成"帧"(51 从机协议)
4.1 为什么从"主动打印"改成"被动应答"
原厂例程是"自己读、自己打印 ASCII",只适合人看。要接进 ZigBee 网络,必须让 51 变成从机:
| 对比项 | 主动打印(原厂) | 被动应答(本次改造) |
|---|---|---|
| 谁发起 | 51 自己定时发 | CC2530 问一次,51 答一次 |
| 数据格式 | ASCII 文本 | 二进制定长帧 |
| 出错处理 | 无 | 哨兵值 + CRC |
| 双方耦合 | 松(谁都不用管对方) | 紧(波特率、帧格式必须完全一致) |
为什么这样更好:CC2530 的采样周期可以随时改(0.5 s / 2 s),51 不用重烧;而且只有需要时才产生串口流量,功耗和干扰都更低。
4.2 读指令:6 个字节
CC2530 → 51:
01 03 00 2A 00 00
│ │ │ └─┬─┘ └─┬─┘
│ │ │ │ └── 占位,恒 0
│ │ └─────┼──────── 长度高字节(未使用)
│ │ └────────── ★ 触发标志 0x2A:这是"读温湿度"的暗号
│ └──────────────────── 功能码 0x03(读寄存器)
└──────────────────────── 从机地址51 端判定条件(main.c 第 120 行附近)四条全中才应答:
if (uart1_wr >= 6) /* ① 收够 6 字节 */
{
if (RX0_Buffer[0] == TH_SLAVE_ADDR && /* ② 地址对 0x01 */
RX0_Buffer[1] == 0x03 && /* ③ 功能码对 */
RX0_Buffer[3] == TH_TRIGGER_BYTE) /* ④ 触发字节 = 0x2A */
{
TH_SendFrame(RX0_Buffer[0]);
}
}为什么是 0x2A? 原厂例程用的是
0x14,而且它同时还会打印 ASCII 温湿度——两路输出挤在同一根 TX 线上。 本工程把触发字节改成0x2A,与光敏板、门禁板统一,这样 CC2530 侧一套串口代码能通用; 同时关掉 ASCII 打印,让这根线上只有协议帧。 这个改动看着小,却是「整条链路能不能稳定收帧」的分水岭。
为什么不校验 CRC? 因为这是单板短距离直连,且触发字节
0x2A已经足够区分。 代价是:如果 CC2530 发错帧而恰好命中这四条,51 会回一帧——但上报的数据仍然是真实的, 所以错误影响被限制在"多回一帧"而不是"回错数据"。这是有意的取舍,可以在讨论题里展开。
4.3 响应帧:9 个字节
51 → CC2530:
| 下标 | 内容 | 说明 |
|---|---|---|
[0] | 从机地址 | 原样回填 0x01 |
[1] | 功能码 | 0x03 |
[2] | 数据长度 | 4(后面有 4 个数据字节) |
[3] [4] | 温度 | int16,单位 0.1 ℃,高字节在前,补码 |
[5] [6] | 湿度 | uint16,单位 0.1 %RH,高字节在前 |
[7] [8] | CRC | Modbus CRC16,低字节在前 |
实测帧(可直接抄进串口助手验证):
| 场景 | 温度 | 湿度 | 完整帧 |
|---|---|---|---|
| 25.3 ℃ / 61.4 %RH | 253 (0x00FD) | 614 (0x0266) | 01 03 04 00 FD 02 66 EA 89 |
| −5.3 ℃ / 45.0 %RH | −53 (0xFFCB) | 450 (0x01C2) | 01 03 04 FF CB 01 C2 3B D8 |
| 读取失败 | 0x7FFF | 0xFFFF | 01 03 04 7F FF FF FF D2 67 |
字节序坑:数据是大端(高字节在前),CRC 是小端(低字节在前)。 两者相反,这是 Modbus 的历史遗留,务必分别处理:
cmbus_Sendbuf[3] = (u >> 8) & 0xFF; /* 数据:先高 */ mbus_Sendbuf[7] = Crc_return_data & 0xFF; /* CRC:先低 */ mbus_Sendbuf[8] = (Crc_return_data >> 8) & 0xFF;
4.4 CRC16 Modbus:从零算一遍
算法要点:初值 0xFFFF,每字节异或后循环 8 次,最低位为 1 就右移并异或 0xA001(0x8005 的位反转)。
unsigned int cal_crc(unsigned char *snd, unsigned char num)
{
unsigned char i, j;
unsigned int c, crc = 0xFFFF; /* 初值 */
for (i = 0; i < num; i++)
{
c = snd[i] & 0x00FF;
crc ^= c; /* 本字节并入 */
for (j = 0; j < 8; j++)
{
if (crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001; /* 多项式(位反转后的 0x8005) */
}
else
{
crc >>= 1;
}
}
}
return (crc);
}调用时只算前 7 个字节:Crc_return_data = cal_crc(mbus_Sendbuf, 7);
动手验算:拿 01 03 04 00 FD 02 66 算一遍,结果应该是 0x89EA。 按"低字节在前"装帧就是 EA 89,与上表一致。
建议用 Python 交叉验证,避免手算错:
pythondef crc16_modbus(data): crc = 0xFFFF for b in data: crc ^= b for _ in range(8): crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 return crc print(hex(crc16_modbus(bytes([0x01,0x03,0x04,0x00,0xFD,0x02,0x66])))) # 0x89ea
4.5 哨兵值:让"失败"也能被传输
传感器读取失败时,不能"什么都不发"——那样 CC2530 只能看到超时,分不清是"传感器坏了"还是"线断了"。
所以约定两个哨兵值:
温度 = 0x7FFF (ASCII 意义上也便于肉眼识别:7F FF FF FF)
湿度 = 0xFFFFCC2530 收到后不上报,只打印一行 [51 TH] sensor read FAIL,并把属性保留为上一次的有效值。 这样上层永远看不到凭空的 0 ℃ / 0 %RH。
4.6 主循环结构:两个节拍
while (1)
{
/* 节拍 1:每 2 秒采一次传感器(满足 AM2321 的最小间隔要求) */
if (++read_tick >= TH_READ_PERIOD) { read_tick = 0; Read_Sensor(); }
/* 节拍 2:每 100 ms 检查一次有没有 CC2530 的读指令 */
if (flag == 1) { ... TH_SendFrame(...); flag = 0; uart1_wr = 0; }
delay_ms(TH_LOOP_MS); /* 100 ms */
}为什么用"缓存 + 应答"而不是"收到指令才去读传感器"? 因为 Read_Sensor() 要阻塞约 5 ms(3 ms 起始 + 应答 + 40 bit),而 CC2530 等应答的耐心只有几十毫秒。 提前缓存好,应答时只需装帧发送(约 9 × 1.04 ms ≈ 9.4 ms),响应快且时序稳定。
4.7 接收中断的三个坑(原厂代码里全都有)
UART1_RCV 中断是学生最容易写崩的地方。本工程改了 3 处,请对照理解:
| # | 原厂写法 | 问题 | 本工程写法 |
|---|---|---|---|
| 1 | 无帧头同步 | 上电/烧录残留的杂字节会让 RX0_Buffer[0] 不是 0x01,整帧错位 | 只有 uart1_wr==0 时收到 0x01 才开始存 |
| 2 | 无边界检查 | RX0_Buffer[uart1_wr++] 一直加,超过 BUF_LENTH 会踩到相邻变量 | if (uart1_wr >= BUF_LENTH) uart1_wr = 0; |
| 3 | flag 只在成功分支清零 | 校验不通过时 flag 永远为 1,主循环死转 | 处理完无条件 flag=0; uart1_wr=0; |
第 1 条的代码:
/* 只有 0x01 开始收,其余字节在缓冲区为空时一律丢弃 */
if (!(uart1_wr == 0 && SBUF != TH_SLAVE_ADDR))
{
if (uart1_wr >= BUF_LENTH) { uart1_wr = 0; }
RX0_Buffer[uart1_wr++] = SBUF;
flag = 1;
}4.8 收满 ≠ 收全:一个真实的时序陷阱
flag 在收到第一个字节时就置 1,主循环跳进处理分支——但此时后面的 5 个字节可能还在路上。 9600 波特率下 6 个字节要 6.25 ms。
原厂例程的做法是 delay_ms(5) 死等 5 ms —— 5 ms 不够 6.25 ms,最后一个字节可能还没进来, 于是 RX0_Buffer 里是半帧加残留,装出来的 CRC 自然是错的。
本工程的做法是「短延时 + 长度判定」:
delay_ms(3); /* 短延时,覆盖大部分字节 */
if (uart1_wr >= 6) { ... } /* 关键:长度够才处理,不够整帧丢弃 */能稳的根本原因不是延时,而是主循环的 100 ms 节拍: 主循环大部分时间停在 delay_ms(TH_LOOP_MS) 里,等它回到判定点时,6 个字节早就全收完了。 delay_ms(3) 只是应付"恰好中断在延时窗口里"的边界情况。
设计原则:凡是"收完一整帧才能处理"的协议,必须判长度或判结束符, 不能只靠一个「收到数据」标志,更不能靠「睡一会儿应该就够了」。 这是嵌入式串口编程里最常见的翻车点。
✅ 检查点 3(串口调试的核心练习)
目标:不接 CC2530,只用串口助手和 51 完成一次完整问答。
- 51 保持
TH_DEBUG_PRINTF = 0,USB-TTL 接好(交叉 + 共地)。 - 串口助手设 9600、8N1,切 HEX 发送 / HEX 显示。
- 发送:
01 03 00 2A 00 00 - 应收到 9 字节,例如
01 03 04 00 FD 02 66 EA 89 - 自己动手验算:
[3][4]=00 FD→ 253 → 25.3 ℃,和你房间温度对得上吗?[5][6]=02 66→ 614 → 61.4 %RH- 用 Python 或手算 CRC,检查
[7][8]是否为EA 89
- 故障注入练习(重要):
- 发
01 03 00 2B 00 00(触发字节改成 0x2B)→ 应该没有响应,验证第 ④ 条判定生效 - 发
02 03 00 2A 00 00(地址改 0x02)→ 应该没有响应 - 只发
01 03(2 字节)→ 应该没有响应,验证长度判定
- 发
- 对着传感器哈气,再发一次,看
[5][6]是否上升。
做不出这一步,后面全都会失败。这一步通过 = 51 侧完全可信。
5. 第 4 步:CC2530 收帧
5.1 打开宏
编辑 zcl_sampletemperaturesensor.h,只开温湿度这一行:
/* #define ZCL_SENSOR_TYPE_ILLUMINANCE */ /* 板1 光敏板 */
#define ZCL_SENSOR_TYPE_TEMP_HUMI /* 板2 温湿度板 ★本次用这个 */
/* #define ZCL_SENSOR_TYPE_DOOR */ /* 板3 门禁板 */
/* #define ZCL_SENSOR_TYPE_ACTUATOR */ /* 板4 执行器 */⚠️ 只能开一个。这四个宏在代码里是
#if / #elif互斥链, 同时开两个时只有第一个生效,编出来的固件和你想的不一样,而且不报错。 改完必须 Rebuild All。
5.2 UART0 初始化:为什么要覆盖 MT 的 38400
Z-Stack 默认把 UART0 交给 MT 层(Z-Tool 调试用),波特率 38400。而 51 是 9600。
static void zclSensor_TH_UartInit(void)
{
halUARTCfg_t uartConfig;
uartConfig.configured = TRUE;
uartConfig.baudRate = ZCL_TH_UART_BAUDRATE; /* 9600 */
uartConfig.flowControl = FALSE;
uartConfig.flowControlThreshold = 0;
uartConfig.rx.maxBufSize = ZCL_TH_UART_RX_BUF_LEN; /* 16 */
uartConfig.tx.maxBufSize = ZCL_TH_UART_RX_BUF_LEN;
uartConfig.idleTimeout = 6;
uartConfig.intEnable = TRUE;
uartConfig.callBackFunc = zclSensor_TH_UartRxCB; /* ★ 回调 */
HalUARTOpen(ZCL_TH_UART_PORT, &uartConfig); /* PORT_0 */
}三个后果,必须知道:
| 后果 | 说明 |
|---|---|
| Z-Tool 不能再用 | 回调被我们覆盖了;MT 的串口通道等于让给了 51 |
| 不能用串口打印调试 | 波特率是 9600、线接在 51 上,PC 插不进来 |
| 调试只能走 JTAG | 这正是第 6 节要讲的 IAR Terminal I/O |
CC2530 只有 一个 UART0。这是硬件限制,不是软件配置问题。 "既想和 51 通信、又想往 PC 打印"在同一时刻做不到——这个认知会直接影响你选调试手段。
5.3 发读指令
static void zclSensor_TH_UartSendReadCmd(void)
{
uint8 cmd[6];
cmd[0] = 0x01; /* 从机地址 */
cmd[1] = 0x03; /* 功能码 */
cmd[2] = 0x00;
cmd[3] = 0x2A; /* ★ 触发标志,与 51 端 TH_TRIGGER_BYTE 必须一致 */
cmd[4] = 0x00;
cmd[5] = 0x00;
HalUARTWrite(ZCL_TH_UART_PORT, cmd, sizeof(cmd));
}为什么不用 HalUARTWrite 的字符串版本? 因为帧里有 0x00,字符串会被截断。必须用带长度的版本。
5.4 接收回调:只负责搬运,不负责判断
static void zclSensor_TH_UartRxCB(uint8 port, uint8 event)
{
uint8 ch;
/* ① 只处理"有数据"事件,忽略 TX_EMPTY */
if (!(event & (HAL_UART_RX_FULL | HAL_UART_RX_ABOUT_FULL | HAL_UART_RX_TIMEOUT)))
{
return;
}
while (Hal_UART_RxBufLen(ZCL_TH_UART_PORT) > 0)
{
if (HalUARTRead(ZCL_TH_UART_PORT, &ch, 1) != 1) break;
/* ② 自检记账:不管认不认这帧,先数下来(诊断用,见 8.1) */
zclSensor_TH_RxBytes++;
zclSensor_TH_LastByte = ch;
/* ③ 帧头同步:缓冲区为空时,不是 0x01 就丢 */
if (zclSensor_TH_UartRxCnt == 0 && ch != 0x01) continue;
if (zclSensor_TH_UartRxCnt >= ZCL_TH_UART_RX_BUF_LEN)
{
zclSensor_TH_UartRxCnt = 0;
}
zclSensor_TH_UartRxBuf[zclSensor_TH_UartRxCnt++] = ch;
/* ④ 满 9 字节 → 置事件,交给主循环解析 */
if (zclSensor_TH_UartRxCnt >= ZCL_TH_FRAME_LEN)
{
zclSensor_TH_GotFrame = 1;
osal_set_event(zclSampleTemperatureSensor_TaskID, ZCL_TH_UART_RX_EVT);
zclSensor_TH_UartRxCnt = 0;
}
}
}铁律:中断/回调里不做解析,只做搬运。 解析要算乘除、要调协议栈发包,耗时几十毫秒。在回调里做会让整个 Z-Stack 卡顿, 表现为节点莫名掉线。正确做法是
osal_set_event()抛给主循环。
5.5 事件循环里解析
if (events & ZCL_TH_UART_RX_EVT)
{
if (zclSensor_TH_GotFrame)
{
zclSensor_TH_UartParseFrame(zclSensor_TH_UartRxCnt);
}
zclSensor_TH_GotFrame = 0;
zclSensor_TH_UartRxCnt = 0;
return (events ^ ZCL_TH_UART_RX_EVT);
}5.6 解析:校验 → 拼字节 → 换算
为便于阅读,下面用
buf代替真实变量名zclSensor_TH_UartRxBuf,用GotFrame代替zclSensor_TH_GotFrame。
/* ① 帧结构校验:功能码 0x03、长度 4 */
if (buf[1] != 0x03 || buf[2] != 4) { GotFrame = 0; return; }
/* ② 拼成 16 位(大端:高字节在前) */
t10 = (int16)( ((uint16)buf[3] << 8) | (uint16)buf[4] ); /* 温度,有符号 */
h10 = (uint16)( ((uint16)buf[5] << 8) | (uint16)buf[6] ); /* 湿度,无符号 */
/* ③ 哨兵值:51 读传感器失败,本次不上报 */
if (t10 == 0x7FFF || h10 == 0xFFFF) { /* 打印 FAIL,return */ }
/* ④ 限幅到 AM2321 量程:-40.0 ~ +80.0 ℃,0 ~ 100.0 %RH */
if (t10 < -400) t10 = -400;
if (t10 > 800) t10 = 800;
if (h10 > 1000) h10 = 1000;
/* ⑤ 0.1 → 0.01(ZCL 强制单位) */
zclSensor_TemperatureMeasuredValue = (int16)(t10 * 10);
zclSensor_HumidityMeasuredValue = (uint16)(h10 * 10);为什么必须限幅? 假设传输错位,51 回了 0x7FFE(比哨兵少 1,所有校验都通过)。这个值 ×10 之后是 327,660,远超 int16 的 32,767,赋值时会被截断成一个说不清的数——可能是负数,也可能是个离谱的高温。 上报给协调器不仅难看,还会直接触发上位机的报警逻辑。限幅是"最后一道防线"。
t10必须声明为int16,不能是uint16。 第 ② 步用(uint16)中转是对的(那只是拼位,不涉及数值语义); 但变量本身一旦是uint16,if (t10 < -400)就永远为假,零下温度的限幅完全失效。 这类错误编译器只会给一句"比较恒假"的告警,很容易被刷屏的其他信息淹没。 养成习惯:凡是有符号量,从声明开始就要有符号。
5.7 上报
/* 温度簇 0x0402,属性 MeasuredValue,int16,单位 0.01℃ */
zclSampleTemperatureSensor_ReportAttr(ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT,
ATTRID_MS_TEMPERATURE_MEASURED_VALUE,
ZCL_DATATYPE_INT16,
(void *)&zclSensor_TemperatureMeasuredValue,
sizeof(int16));
/* 湿度簇 0x0405,属性 MeasuredValue,uint16,单位 0.01%RH */
zclSampleTemperatureSensor_ReportAttr(ZCL_CLUSTER_ID_MS_RELATIVE_HUMIDITY,
ATTRID_MS_RELATIVE_HUMIDITY_MEASURED_VALUE,
ZCL_DATATYPE_UINT16,
(void *)&zclSensor_HumidityMeasuredValue,
sizeof(uint16));✅ 检查点 4
| 检查项 | 期望 |
|---|---|
| 节点上电 | [BOOT] TH pan=0x1234 |
| 入网过程 | state=2 → 3 → 5 → 6(有 3 和 5 说明 NV 是干净的) |
| 解析成功 | [51 TH] T=25.3 C H=61.4 %RH,约每 0.5 s 一行 |
| 上报 | 数值变化时出现 [RPT] 相关行 |
没出现 [51 TH] 就是失败,直接跳到第 8 节排查。
6. 第 5 步:CC2530 的 printf —— IAR Terminal I/O(调试手段二)
6.1 两条调试通道,别搞混
这是本实验最容易被忽视、也最影响效率的一点:
| 51 单片机的 printf | CC2530 的 printf | |
|---|---|---|
| 输出去哪 | 物理串口 P3.1(真实引脚) | JTAG / C-SPY 仿真终端(不出引脚) |
| PC 怎么接 | USB-TTL 接到 P3.0/P3.1 | 不用接线,CC Debugger 已连着 |
| 软件看哪 | 串口助手 / 超级终端 | IAR 菜单 View → Terminal I/O |
| 是否需要调试器 | 不需要(烧 hex 独立跑也能打印) | 必须 Download and Debug,烧 hex 独立跑看不到 |
| 是否占用 UART0 | 占用(会与协议帧打架) | 不占用(UART0 完整留给 51) |
| 适合 | 验证传感器读数、标定阈值 | 验证解析逻辑、看入网状态、看上报 |
一句话记住:
51 的 printf 走铜线,CC2530 的 printf 走 JTAG。 所以 CC2530 侧可以放心大胆地打印,不会影响和 51 的通信。
6.2 打印是怎么走到终端的
ZC_DBG_STR("T=") → putchar('T') → C 库 __write → C-SPY 调试代理 → IAR Terminal I/O 窗口
(走 JTAG,不碰 P0_2/P0_3)关键在于:putchar 的目标是调试器,不是串口。这也是为什么它和 HalUARTWrite 完全不同。
6.3 操作步骤
| 步 | 操作 |
|---|---|
| ① | 工程配置选 EndDeviceEB |
| ② | 菜单 Project → Options → Debugger,Driver 选 Texas Instruments |
| ③ | 确认 Project → Options → General Options → Library Configuration 里 Library 不是 None(本工程是 CLIB) |
| ④ | Project → Download and Debug(不是 Download only) |
| ⑤ | 菜单 View → Terminal I/O 打开窗口 |
| ⑥ | 按 F5 全速运行 |
⚠️ 第 ④ 步是最容易错的。烧完 hex 就退出调试器,Terminal I/O 立刻断掉, 学生常以为"打印坏了"。实际上打印通道是调试器提供的,调试器一停,通道就没了。
另外:JTAG 只连 4 根线(VCC/GND/DD/DC),不占 UART0。所以调试期间 51 ↔ CC2530 的通信照常进行。
6.4 实测坑:本工程里 printf 的格式符不展开
这是必须让学生亲眼看到的一个现象。
本工程链接的是 IAR CLIB(cl-pli-blxd-1e16x01.r51)。实测:
printf("x=%u\r\n", 253); /* 实际输出: x=u ← 格式符原样输出了 'u' */
printf("0x%02x\r\n", 0xAB); /* 实际输出: 0x02x ← 只有普通字符出来了 */现象:% 后面的转换字符被当成普通字符原样输出,参数根本没被格式化。
为什么会这样:printf 的格式化能力是可裁剪的。IAR 允许选 printf formatter(none / small / medium / large), 选得越小,占的 Flash 越少,但支持的转换符越少;选 none 时 printf 退化成"只会输出普通字符"。 本工程为了给 Z-Stack 省空间,链接的是精简变体,于是丢了格式化能力。
不要凭猜测下结论——让学生自己查证: 打开
Project → Options → General Options → Library Configuration,看printf formatter这一栏到底选了什么; 再打开Debug/List/*.map,搜printf和?printf,看实际链进来的是哪个库符号。 这一步比"记住结论"重要得多——同一个printf,在不同工程里行为可以完全不同。
验证实验(建议课堂当场做):
- 在
zclSensor_TH_UartParseFrame()里插一句:cprintf("T=%d\r\n", 253); Download and Debug→ Terminal I/O- 观察输出:如果看到
T=d或T=u之类的怪结果,就说明格式符没生效 - 想验证"是库的问题而不是我写错了",把 formatter 改成
large重新编译,再看同一句——如果这时正常了,结论就闭合了 - 然后在正式固件里改回精简配置,用逐字符输出,立刻正常:c
ZC_DBG_STR("T="); ZC_DBG_U16(253); ZC_DBG_STR("\r\n");
结论:本工程不要用 printf 的转换符,改用自带的 ZC_DBG_* 宏。 宏是"字符串 + 逐字符 putchar",不依赖任何格式化引擎,换任何库配置都不会失灵。
6.5 ZC_DBG_* 宏怎么用
| 宏 | 输出 | 示例结果 |
|---|---|---|
ZC_DBG_STR("abc") | 字符串(逐字符 putchar) | abc |
ZC_DBG_U16(v) | 无符号十进制 0~65535 | ZC_DBG_U16(253) → 253 |
ZC_DBG_S16(v) | 有符号十进制 | ZC_DBG_S16(-53) → -53 |
ZC_DBG_U8P2(v) | 固定两位补零 | ZC_DBG_U8P2(7) → 07 |
ZC_DBG_HEX8(v) | 两位大写十六进制 | ZC_DBG_HEX8(0xAB) → AB |
ZC_DBG_HEX16(v) | 四位大写十六进制 | ZC_DBG_HEX16(0x1234) → 1234 |
组合出温度打印(工程里的真实写法):
ZC_DBG_STR("\r\n[51 TH] T=");
ZC_DBG_S16((int16)(t10 / 10)); /* 整数部分,可能为负 */
ZC_DBG_STR(".");
ZC_DBG_U16((uint16)((t10 >= 0) ? (t10 % 10) : (-t10 % 10))); /* 小数部分,取绝对值 */
ZC_DBG_STR(" C H=");
ZC_DBG_U16((uint16)(h10 / 10));
ZC_DBG_STR(".");
ZC_DBG_U16((uint16)(h10 % 10));
ZC_DBG_STR(" %RH\r\n");输出:[51 TH] T=25.3 C H=61.4 %RH
为什么宏而不是函数? 用函数的话,某个构建配置用不到它,IAR 会报
Pe177: variable was declared but never referenced。 宏在预处理阶段展开,不产生符号,就没有告警。
6.6 加一个自己的调试打印(练习)
要求:在解析成功后额外打印一行原始字节,用来确认帧内容。
参考答案:
ZC_DBG_STR("[RAW] ");
ZC_DBG_HEX8(zclSensor_TH_UartRxBuf[3]); ZC_DBG_STR(" ");
ZC_DBG_HEX8(zclSensor_TH_UartRxBuf[4]); ZC_DBG_STR(" ");
ZC_DBG_HEX8(zclSensor_TH_UartRxBuf[5]); ZC_DBG_STR(" ");
ZC_DBG_HEX8(zclSensor_TH_UartRxBuf[6]); ZC_DBG_STR("\r\n");输出:[RAW] 00 FD 02 66
对照检查点 3 里串口助手收到的帧,两者应完全一致——这是"51 发的"和"CC2530 收的"唯一可靠的比对方式。
6.7 内建自检:NO FRAME
工程里内置了一个"5 秒体检"机制:每 10 次采样(10 × 500 ms = 5 s)检查一次, 只有这 5 秒一帧都没解析成功时才打印:
[51 TH] NO FRAME tx=10 rxB=0 last=0x00
│ │ └── 最后收到的字节
│ └──────── 本窗口共收到多少字节(不管成不成帧)
└────────────── 本窗口发了几条读指令正常工作时这行一行都不会打,所以它出现就一定是故障信号。判读表见第 8.1 节。
6.8 补充技巧:用串口助手"旁听" CC2530 与 51 之间的线
CC2530 侧不能用串口打印,但不代表不能用串口调试。串口线是共享介质,只要只听不说,就能看到线上的一切。
接法(关键:只接 RX,不接 TX):
51 P3.1 (TXD) ──────────────┬──────────────→ CC2530 P0_2 (RX)
│
└──→ USB-TTL 的 RX ← 只接这一根
USB-TTL 的 GND ── 共地
USB-TTL 的 TX ── ✗ 悬空,不要接能听到什么:
| 监听哪根线 | 串口助手(HEX,9600)能看到 | 说明 |
|---|---|---|
| 51 的 TX(P3.1) | 每 0.5 s 一帧 01 03 04 .. .. .. .. CRCL CRCH | 证明 51 在应答;帧内容可与 Terminal I/O 里的 [RAW] 对比 |
| CC2530 的 TX(P0_3) | 每 0.5 s 一串 01 03 00 2A 00 00 | 证明 CC2530 在主动发读指令(哪怕 51 没接) |
这一招能解决的典型僵局:
"Terminal I/O 里一直
NO FRAME,到底是 CC2530 没发指令,还是 51 没回?"
把 TTL 并在 P0_3 上:
- 能看到
01 03 00 2A 00 00反复出现 → CC2530 在发,问题在 51 或回程线 - 一个字节都看不到 → CC2530 根本没发,查
ZCL_SENSOR_REPORT_EVT定时器、zclSensor_TH_UartInit()是否被调用
再并在 P3.1 上:
- 有帧 → 51 回了,问题在 CC2530 的接收回调或解析
- 没帧 → 51 没回,回到检查点 3 单独验 51
⚠️ 不要把 USB-TTL 的 TX 也接上去。两个 TX 直接并联会互相打架(一个输出高、一个输出低就是短路), 轻则通信错乱,重则烧引脚。旁听只接 RX + GND 两根线。
✅ 检查点 5
- 在 Terminal I/O 里确认
[51 TH]每 0.5 s 一行。 - 拔掉 51 的 TX 线(P3.1),观察 5 秒后是否出现
NO FRAME tx=10 rxB=0 last=0x00。 - 插回线,确认
NO FRAME消失、[51 TH]恢复。 - 旁听练习:把 USB-TTL 的 RX 接到 CC2530 的 P0_3(只接 RX + GND),串口助手 HEX 9600, 应能看到
01 03 00 2A 00 00每 0.5 s 出现一次。 - 思考:如果把 printf formatter 改成
large,printf("%d")就能正常工作了。 那么工程里还需要ZC_DBG_*宏吗?说出你的理由。 (提示:想想代码体积、可移植性,以及"换个工程配置就失灵"这件事有多难排查)
7. 第 6 步:什么时候才发上报(收发策略)
节点不是"读到就发"。三个参数决定行为:
| 参数 | 值 | 作用 |
|---|---|---|
ZCL_SENSOR_SAMPLE_PERIOD | 500 ms | 多久问一次 51 |
ZCL_SENSOR_TEMP_DEADBAND | 5(0.5 ℃) | 温度变化小于它 → 只更新属性,不发包 |
ZCL_SENSOR_HUMI_DEADBAND | 20(2.0 %RH) | 湿度变化小于它 → 不发包 |
ZCL_SENSOR_HB_REPORT | 10000 ms | 连续 10 s 没变化也补发一帧(心跳兜底) |
为什么要有死区:AM2321 自身的噪声大约就是 ±0.1~0.3 ℃。没有死区的话,数值会在 25.3/25.4 之间反复跳, 每次跳都发一帧,网络里全是无效流量,上位机的趋势图也会变成锯齿。
为什么要有心跳:上位机超过 20 s 收不到帧就判定节点掉线。心跳保证"环境真的没变"时也不会误报。
⚠️ 温湿度天然慢半拍:51 端每 2 s 才采一次传感器并缓存(AM2321 的硬性要求), CC2530 问得再快(0.5 s)也只是重复读同一份缓存。 所以端到端延时 ≈ 最多 2 s(缓存陈旧度)+ 100 ms(51 主循环节拍)+ 16 ms(问答)+ 无线与上位机开销 ≈ 2 ~ 2.5 s。 这是物理限制,不是程序慢。想快只能换传感器(如 SHT30,I²C,可 1 s 内多次读)。
✅ 检查点 6
- 用手捂住传感器,观察 Terminal I/O:
- 温度上升超过 0.5 ℃ 或湿度上升超过 2 %RH 时,
[RPT]立刻出现 - 变化很慢时会出现
[SKIP] dT=3 dH=12 < deadband
- 温度上升超过 0.5 ℃ 或湿度上升超过 2 %RH 时,
- 手拿开后静置,约 10 s 会看到一次心跳上报。
- 把
ZCL_SENSOR_TEMP_DEADBAND改成0,重新编译,观察上报频率的变化(然后改回来)。
8. 联调与故障定位
8.1 NO FRAME 三态判读表(最有用的一张表)
rxB | last | 含义 | 下一步 |
|---|---|---|---|
| 0 | 0x00 | CC2530 一个字节都没收到 | ①TX/RX 是否交叉 ②是否共地 ③51 是否上电 ④51 是否烧了 AM2321.hex |
| > 0 | 0x54('T') | 收到的是 ASCII 文本 | 51 里跑的还是原厂例程(自己在 printf),必须烧 out/AM2321.hex |
| > 0 | 其他 | 收到字节但拼不成帧 | 帧头/长度/波特率不一致;或 TH_DEBUG_PRINTF 忘了改 0 |
| 正常 | — | 一行都不打 | 这就是成功状态 |
8.2 分层排查:从物理层往上爬
按顺序做,每一步失败就停下,不要跳步:
① 51 单独能读懂传感器吗?
└─ Keil Watch 看 TH_Temp10 / TH_Humi10 是否合理
✗ → 查 AM2321 接线(P1.1)、上拉电阻、3ms 起始脉宽
✓ ↓
② 51 单独能应答串口指令吗?
└─ 串口助手发 01 03 00 2A 00 00,是否收到 9 字节
✗ → 查波特率 9600、TH_DEBUG_PRINTF 是否为 0、判定四条件
✓ ↓
③ CC2530 能收到帧吗?
└─ Terminal I/O 有 [51 TH] 行吗?有 NO FRAME 吗?
✗ → 按 8.1 表判读
✓ ↓
④ 值算对了吗?
└─ [51 TH] 的温度和 ② 里收到的帧换算结果一致吗?
✗ → 查字节序(大端)、符号位、×10 换算
✓ ↓
⑤ 上报出去了吗?
└─ 协调器侧有 [RX] node=... T=... 吗?
✗ → 查簇 ID(0x0402/0x0405)、节点是否 state=6
✓ → 本实验完成8.3 常见故障速查
| 现象 | 原因 | 处理 |
|---|---|---|
温度恒为 0x7FFF | AM2321 没应答 | ①SDA 是否上拉 ②模块供电 ③两次读取间隔是否满足 ≥ 2 s |
| 温度是巨大正数(如 65483) | 没拆 bit15 符号位 | 用 if (data[2] & 0x80) 分支取反 |
湿度恒定 0xFFFF | 校验和总是不通过 | 多半是延时不准(晶振不是 11.0592 MHz) |
| 温度像是差了 10 倍 | 少乘或多乘了 10 | 帧里是 0.1,ZCL 是 0.01,只在上报前 ×10 |
| 零下温度错乱 | 用了 uint16 接温度 | 改成 int16 |
每 5 s 出现 NO FRAME rxB=45 last=0x54 | 51 在打印 ASCII | TH_DEBUG_PRINTF 改 0 |
[51 TH] 有但值不变 | 51 缓存没更新 | 查 TH_READ_PERIOD 与 Read_Sensor() 返回值 |
| 串口助手收到乱码 | 波特率或晶振不对 | 9600 8N1,晶振 11.0592 MHz |
| Terminal I/O 一片空白 | 不是 Debug 模式,或没打开窗口 | Download and Debug + View → Terminal I/O |
printf 输出 x=u | 工程链接的库里 printf formatter 被裁剪 | 改用 ZC_DBG_* 宏,见 6.4 |
| 旁听 P0_3 一个字节都没有 | CC2530 没发出读指令 | 查 ZCL_SENSOR_REPORT_EVT 定时器与 zclSensor_TH_UartInit(),见 6.8 |
| 节点入网后没数据 | NV 里是旧网络 | IAR 勾 Erase flash 重烧,看到 state=3/5 才算干净 |
9. 实验报告要求
9.1 必做内容
- 数据链路图:画出三段数据流,标注每段的单位与字节序。
- 实测数据表:至少 5 组「raw 5 字节 → 计算出的温湿度 → CC2530 打印值」,三列必须自洽。
- CRC 验算:任选一组实测帧,用 Python 或手算验证 CRC,给出过程。
- 两条调试通道对比:用表格回答"51 的 printf 与 CC2530 的 printf 有何不同", 并说明为什么 CC2530 不能用串口 printf。
- 故障复现记录:任选 2 个故障(建议选
TH_DEBUG_PRINTF忘关、符号位没处理), 记录现象、定位过程、修复方法。
9.2 思考题
- 51 为什么采用「2 秒采样缓存 + 应答」而不是「收到指令才读传感器」? 如果 CC2530 每 100 ms 问一次,会出现什么问题?
- 51 的读指令判定没有校验 CRC。请分析这个设计在什么情况下会出问题, 以及如果要求加上 CRC,需要改动哪几处。
ZCL_TH_TEMP_MIN_10/MAX_10限幅会不会掩盖真实的故障?说出你的判断和理由。- 如果把 AM2321 换成 I²C 的 SHT30,51 端、CC2530 端、协议帧分别要改什么?
- CC2530 只有一个 UART0。如果既要和 51 通信、又要往 PC 打印调试信息, 有哪几种可行方案?各自的代价是什么?
附录 A 速查卡
A.1 引脚与参数
| 项目 | 值 |
|---|---|
| AM2321 数据线 | 51 的 P1.1(单总线,非 I²C) |
| 51 串口 | P3.0 = RXD,P3.1 = TXD |
| CC2530 串口 | UART0 Alt-1:P0_2 = RX,P0_3 = TX |
| 接线方式 | 交叉:51 P3.1 → CC2530 P0_2;51 P3.0 ← CC2530 P0_3;必须共地 |
| 波特率 | 9600 8N1(两端一致) |
| 51 晶振 | 11.0592 MHz(BRT 重装值 184) |
| 读指令 | 01 03 00 2A 00 00 |
| 响应帧 | 01 03 04 TH TH HH HH CRCL CRCH(9 字节) |
| 采样周期 | 51:2 s;CC2530:0.5 s |
| 上报死区 | 温度 0.5 ℃ / 湿度 2.0 %RH;心跳 10 s |
| ZCL 簇 | 温度 0x0402,湿度 0x0405 |
A.2 关键宏(改之前先想清楚)
| 文件 | 宏 | 默认 | 作用 |
|---|---|---|---|
zcl_sampletemperaturesensor.h | ZCL_SENSOR_TYPE_TEMP_HUMI | 打开 | 决定编哪块板的固件,只能开一个 |
zcl_sampletemperaturesensor.h | ZCL_SENSOR_SAMPLE_PERIOD | 500 | CC2530 问 51 的间隔 |
zcl_sampletemperaturesensor.h | ZCL_SENSOR_TEMP_DEADBAND | 5 | 0.5 ℃ 内不重发 |
STC.../src/main.c | TH_DEBUG_PRINTF | 需自行添加 | 1 = 打印(不能接 CC2530)/ 0 = 协议模式,见 3.3 |
STC.../src/main.c | TH_UART_BANNER | 0 | 上电欢迎语,必须保持 0 |
STC.../src/main.c | TH_READ_PERIOD | 20 | ×100 ms = 2 s 采样周期 |
A.3 三条最容易犯的错
- 以为 AM2321 是 I²C → 它只有一根 SDA,走自定义单总线时序。
- 调试打印忘了关 → CC2530 收到 ASCII,
NO FRAME rxB=45 last=0x54。 - 用了串口 printf 去调 CC2530 → UART0 已经被 51 占满,必须改用 IAR Terminal I/O。
附录 B 文件位置
实验五综合实验箱/
├── STC单片机读取温湿度传感器程序/
│ ├── AM2321.uvproj ← 51 工程(Keil C51)
│ ├── src/main.c ← 全部逻辑(GBK 编码)
│ └── out/AM2321.hex ← STC-ISP 烧这个
├── cc2530部分/Z-Stack 3.0.1/Projects/zstack/HomeAutomation/SampleTemperatureSensor/
│ ├── Source/zcl_sampletemperaturesensor.h ← 节点类型宏、采样参数
│ ├── Source/zcl_sampletemperaturesensor.c ← UART、解析、上报、调试宏
│ ├── Source/zcl_sampletemperaturesensor_data.c ← 属性表、簇列表
│ └── CC2530DB/SampleTemperatureSensor.ewp ← IAR 工程
├── 固件/THNode.hex ← 温湿度节点固件(SmartRF / IAR 烧)
├── 51单片机-采集板串口协议.md ← 三块采集板的完整帧协议
└── 四板四节点-组网与烧录说明.md ← 组网、NV、Terminal I/O、常见问题本指南与以下文档配套使用:
- 帧格式细节、其他采集板对照 →《51单片机-采集板串口协议.md》
- 组网、NV 擦除、
[BOOT]/[NWK]/[HB]日志判读 →《四板四节点-组网与烧录说明.md》- 节点上报之后的数据如何到达 PC →《协调器-PC-二进制协议.md》(本实验不涉及)