Skip to content

实验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 + AM23211板上 AM2321 已接 P1.1
ZigBee 节点板CC2530 模块1板载 UART0:P0_2 = RX,P0_3 = TX
协调器CC2530 模块 + USB 转串口1本实验只用来确认节点入网
下载器CC Debugger1配 IAR EW8051 8.0
USB-TTLCH340 / CP21021调 51 侧时用,3.3 V 电平档
杜邦线母-母4 根至少 3 根要能交叉接

0.3 前置知识自检 ​

开始前请确认你能回答:

  1. UART 一帧 10 bit 里,起始位、数据位、停止位各在哪?
  2. 波特率 9600 时,传 1 个字节要多久?(答案:约 1.04 ms)
  3. "交叉接线"指 TX 接对方的什么?(答案:RX)
  4. 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 → 515 字节原始值(40 bit)0.1 ℃ / 0.1 %RH51:拆符号位、算校验和
② 51 → CC25309 字节 Modbus 帧0.1 ℃ / 0.1 %RH51:原样装帧,不做缩放
③ CC2530 → 协调器ZCL 属性0.01 ℃ / 0.01 %RHCC2530:×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 硬件连接 ​

AM232151 单片机说明
SDA(数据)P1.1单总线,需 4.7 kΩ 上拉到 VCC
VCC5 V
GNDGND

⚠️ 不是 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 µsSensor_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 还是高,一刀切开:

c
delay_30us();          /* 卡在 30µs 采样点 */
tmp = 0;
if (Sensor_SDA)        /* 还是高 → 1 */
{
    tmp = 1;
}

2.4 五个字节怎么变成温度湿度 ​

c
Sensor_Data[0]  湿度高字节
Sensor_Data[1]  湿度低字节
Sensor_Data[2]  温度高字节   ← bit15 是符号位!
Sensor_Data[3]  温度低字节
Sensor_Data[4]  校验和

湿度(无符号,直接拼):

c
TH_Humi10 = (Sensor_Data[0] << 8) | Sensor_Data[1];      /* 0.1 %RH */

温度(有符号,必须先拆符号位):

c
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,否则会返回上一次的陈旧数据甚至错误数据。

c
#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,把字符塞进串口就行——这叫重定向:

c
#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。

更省事的写法是直接调用现成函数:

c
char putchar(char c)
{
    Uart1_TxByte((unsigned char)c);
    return c;
}

3.3 加一个可以一键关掉的调试开关 ​

直接往协议帧里插 printf 会出大问题,所以必须用宏隔离:

c
/* ===== 调试打印开关 =====
 * 1 = 打开:往串口打印人读文本(此时不能接 CC2530!)
 * 0 = 关闭:串口只跑 9 字节协议帧            */
#define  TH_DEBUG_PRINTF   1

在 main() 的采样分支里这样用(用 #if 包住,不用可变参数宏,任何版本的 C51 都能编):

c
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 660x0266 = 614÷10 = 61.4 %RH ✓
raw 后两字节 00 FD0x00FD = 253÷10 = 25.3 ℃ ✓
raw 末字节 650x65 = 1010x02+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 的概率不高,但前几帧的帧头必然错位,表现为"上电后前几秒数据乱跳"。 本工程把同样的欢迎语收进了一个开关,默认关掉,并在注释里写明原因:

c
#define  TH_UART_BANNER   0       /* 1 = 上电发 "TH board ready"
                                   * 【默认 0,必须保持 0】      */

调试口诀: 接 USB-TTL 看 printf 时 → 拔掉 CC2530;接 CC2530 跑协议时 → TH_DEBUG_PRINTF 改 0。

✅ 检查点 2 ​

  1. 把 TH_DEBUG_PRINTF 改成 0,重新编译下载,串口助手切 HEX 显示。
  2. 此时串口上不该有任何主动输出——一片空白才是对的。
  3. 用串口助手发 01 03 00 2A 00 00(HEX 模式),应立刻收到 9 字节。
  4. 回答:如果不改 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 行附近)四条全中才应答:

c
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]CRCModbus CRC16,低字节在前

实测帧(可直接抄进串口助手验证):

场景温度湿度完整帧
25.3 ℃ / 61.4 %RH253 (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
读取失败0x7FFF0xFFFF01 03 04 7F FF FF FF D2 67

字节序坑:数据是大端(高字节在前),CRC 是小端(低字节在前)。 两者相反,这是 Modbus 的历史遗留,务必分别处理:

c
mbus_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 的位反转)。

c
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 交叉验证,避免手算错:

python
def 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)
湿度 = 0xFFFF

CC2530 收到后不上报,只打印一行 [51 TH] sensor read FAIL,并把属性保留为上一次的有效值。 这样上层永远看不到凭空的 0 ℃ / 0 %RH。

4.6 主循环结构:两个节拍 ​

c
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;
3flag 只在成功分支清零校验不通过时 flag 永远为 1,主循环死转处理完无条件 flag=0; uart1_wr=0;

第 1 条的代码:

c
/* 只有 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 自然是错的。

本工程的做法是「短延时 + 长度判定」:

c
delay_ms(3);                    /* 短延时,覆盖大部分字节 */
if (uart1_wr >= 6) { ... }      /* 关键:长度够才处理,不够整帧丢弃 */

能稳的根本原因不是延时,而是主循环的 100 ms 节拍: 主循环大部分时间停在 delay_ms(TH_LOOP_MS) 里,等它回到判定点时,6 个字节早就全收完了。 delay_ms(3) 只是应付"恰好中断在延时窗口里"的边界情况。

设计原则:凡是"收完一整帧才能处理"的协议,必须判长度或判结束符, 不能只靠一个「收到数据」标志,更不能靠「睡一会儿应该就够了」。 这是嵌入式串口编程里最常见的翻车点。

✅ 检查点 3(串口调试的核心练习) ​

目标:不接 CC2530,只用串口助手和 51 完成一次完整问答。

  1. 51 保持 TH_DEBUG_PRINTF = 0,USB-TTL 接好(交叉 + 共地)。
  2. 串口助手设 9600、8N1,切 HEX 发送 / HEX 显示。
  3. 发送:01 03 00 2A 00 00
  4. 应收到 9 字节,例如 01 03 04 00 FD 02 66 EA 89
  5. 自己动手验算:
    • [3][4] = 00 FD → 253 → 25.3 ℃,和你房间温度对得上吗?
    • [5][6] = 02 66 → 614 → 61.4 %RH
    • 用 Python 或手算 CRC,检查 [7][8] 是否为 EA 89
  6. 故障注入练习(重要):
    • 发 01 03 00 2B 00 00(触发字节改成 0x2B)→ 应该没有响应,验证第 ④ 条判定生效
    • 发 02 03 00 2A 00 00(地址改 0x02)→ 应该没有响应
    • 只发 01 03(2 字节)→ 应该没有响应,验证长度判定
  7. 对着传感器哈气,再发一次,看 [5][6] 是否上升。

做不出这一步,后面全都会失败。这一步通过 = 51 侧完全可信。


5. 第 4 步:CC2530 收帧 ​

5.1 打开宏 ​

编辑 zcl_sampletemperaturesensor.h,只开温湿度这一行:

c
/* #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。

c
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 发读指令 ​

c
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 接收回调:只负责搬运,不负责判断 ​

c
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 事件循环里解析 ​

c
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。

c
/* ① 帧结构校验:功能码 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 上报 ​

c
/* 温度簇 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 单片机的 printfCC2530 的 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)。实测:

c
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,在不同工程里行为可以完全不同。

验证实验(建议课堂当场做):

  1. 在 zclSensor_TH_UartParseFrame() 里插一句:
    c
    printf("T=%d\r\n", 253);
  2. Download and Debug → Terminal I/O
  3. 观察输出:如果看到 T=d 或 T=u 之类的怪结果,就说明格式符没生效
  4. 想验证"是库的问题而不是我写错了",把 formatter 改成 large 重新编译,再看同一句——如果这时正常了,结论就闭合了
  5. 然后在正式固件里改回精简配置,用逐字符输出,立刻正常:
    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~65535ZC_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

组合出温度打印(工程里的真实写法):

c
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 加一个自己的调试打印(练习) ​

要求:在解析成功后额外打印一行原始字节,用来确认帧内容。

参考答案:

c
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 ​

  1. 在 Terminal I/O 里确认 [51 TH] 每 0.5 s 一行。
  2. 拔掉 51 的 TX 线(P3.1),观察 5 秒后是否出现 NO FRAME tx=10 rxB=0 last=0x00。
  3. 插回线,确认 NO FRAME 消失、[51 TH] 恢复。
  4. 旁听练习:把 USB-TTL 的 RX 接到 CC2530 的 P0_3(只接 RX + GND),串口助手 HEX 9600, 应能看到 01 03 00 2A 00 00 每 0.5 s 出现一次。
  5. 思考:如果把 printf formatter 改成 large,printf("%d") 就能正常工作了。 那么工程里还需要 ZC_DBG_* 宏吗?说出你的理由。 (提示:想想代码体积、可移植性,以及"换个工程配置就失灵"这件事有多难排查)

7. 第 6 步:什么时候才发上报(收发策略) ​

节点不是"读到就发"。三个参数决定行为:

参数值作用
ZCL_SENSOR_SAMPLE_PERIOD500 ms多久问一次 51
ZCL_SENSOR_TEMP_DEADBAND5(0.5 ℃)温度变化小于它 → 只更新属性,不发包
ZCL_SENSOR_HUMI_DEADBAND20(2.0 %RH)湿度变化小于它 → 不发包
ZCL_SENSOR_HB_REPORT10000 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 ​

  1. 用手捂住传感器,观察 Terminal I/O:
    • 温度上升超过 0.5 ℃ 或湿度上升超过 2 %RH 时,[RPT] 立刻出现
    • 变化很慢时会出现 [SKIP] dT=3 dH=12 < deadband
  2. 手拿开后静置,约 10 s 会看到一次心跳上报。
  3. 把 ZCL_SENSOR_TEMP_DEADBAND 改成 0,重新编译,观察上报频率的变化(然后改回来)。

8. 联调与故障定位 ​

8.1 NO FRAME 三态判读表(最有用的一张表) ​

rxBlast含义下一步
00x00CC2530 一个字节都没收到①TX/RX 是否交叉 ②是否共地 ③51 是否上电 ④51 是否烧了 AM2321.hex
> 00x54('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 常见故障速查 ​

现象原因处理
温度恒为 0x7FFFAM2321 没应答①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=0x5451 在打印 ASCIITH_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 必做内容 ​

  1. 数据链路图:画出三段数据流,标注每段的单位与字节序。
  2. 实测数据表:至少 5 组「raw 5 字节 → 计算出的温湿度 → CC2530 打印值」,三列必须自洽。
  3. CRC 验算:任选一组实测帧,用 Python 或手算验证 CRC,给出过程。
  4. 两条调试通道对比:用表格回答"51 的 printf 与 CC2530 的 printf 有何不同", 并说明为什么 CC2530 不能用串口 printf。
  5. 故障复现记录:任选 2 个故障(建议选 TH_DEBUG_PRINTF 忘关、符号位没处理), 记录现象、定位过程、修复方法。

9.2 思考题 ​

  1. 51 为什么采用「2 秒采样缓存 + 应答」而不是「收到指令才读传感器」? 如果 CC2530 每 100 ms 问一次,会出现什么问题?
  2. 51 的读指令判定没有校验 CRC。请分析这个设计在什么情况下会出问题, 以及如果要求加上 CRC,需要改动哪几处。
  3. ZCL_TH_TEMP_MIN_10 / MAX_10 限幅会不会掩盖真实的故障?说出你的判断和理由。
  4. 如果把 AM2321 换成 I²C 的 SHT30,51 端、CC2530 端、协议帧分别要改什么?
  5. 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.hZCL_SENSOR_TYPE_TEMP_HUMI打开决定编哪块板的固件,只能开一个
zcl_sampletemperaturesensor.hZCL_SENSOR_SAMPLE_PERIOD500CC2530 问 51 的间隔
zcl_sampletemperaturesensor.hZCL_SENSOR_TEMP_DEADBAND50.5 ℃ 内不重发
STC.../src/main.cTH_DEBUG_PRINTF需自行添加1 = 打印(不能接 CC2530)/ 0 = 协议模式,见 3.3
STC.../src/main.cTH_UART_BANNER0上电欢迎语,必须保持 0
STC.../src/main.cTH_READ_PERIOD20×100 ms = 2 s 采样周期

A.3 三条最容易犯的错 ​

  1. 以为 AM2321 是 I²C → 它只有一根 SDA,走自定义单总线时序。
  2. 调试打印忘了关 → CC2530 收到 ASCII,NO FRAME rxB=45 last=0x54。
  3. 用了串口 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》(本实验不涉及)

aioteer · 物联网专业课程群

💬AI 伴学配置