Zigbee串口代码(2)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

174.9Zigbee串口代码(2)

/* ==================== 51 单片机:物联网51代码/main.c ==================== */
DHT11_Read_Byte(&H, &T);                          /* 读取 DHT11;函数第一个实参接收湿度 H,第二个实参接收温度 T。 */
sprintf(OLED_Buf,"H:%2d%% T:%2dC",H,T);          /* 把湿度和温度格式化到 OLED 字符串缓冲区,%% 用来输出一个百分号。 */
OLED_ShowString(0,4,OLED_Buf,16);                 /* 从 OLED 的 x=0、第 3 个 16 像素行显示本次温湿度。 */

Send_buf[0]=0x55;                                 /* 温度帧第 0 字节写固定帧头 0x55。 */
Send_buf[1]=0x9;                                  /* 第 1 字节写类型码 0x09,课程约定它表示温度。 */
Send_buf[2]=T;                                    /* 第 2 字节写本次读取到的整数温度。 */
Send_buf[3]=Send_buf[0]+Send_buf[1]+Send_buf[2];  /* 第 3 字节写前三个字节的简单累加和;这不是 CRC。 */
Send_buf[4]=0xAA;                                 /* 第 4 字节写固定帧尾 0xAA。 */
for(i=0;i<5;i++)                                  /* 让索引 i 依次取 0~4,准备发送完整的 5 字节温度帧。 */
UART_Send_Byte(Send_buf[i]);                      /* 循环体没有花括号,只有这一条语句会被重复执行五次。 */
Delay350ms();                                     /* 忙等约 350 ms,拉开温度帧和下一帧的发送间隔。 */

Send_buf[0]=0x55;                                 /* 湿度帧第 0 字节仍使用固定帧头 0x55。 */
Send_buf[1]=0xA;                                  /* 第 1 字节写类型码 0x0A,课程约定它表示湿度。 */
Send_buf[2]=H;                                    /* 第 2 字节写本次读取到的整数湿度。 */
Send_buf[3]=Send_buf[0]+Send_buf[1]+Send_buf[2];  /* 计算帧头、类型码和湿度数据的简单累加和。 */
Send_buf[4]=0xAA;                                 /* 写入湿度帧固定帧尾。 */
for(i=0;i<5;i++)                                  /* 遍历湿度帧的五个字节。 */
UART_Send_Byte(Send_buf[i]);                      /* 通过 51 串口逐字节阻塞发送湿度帧。 */
Delay350ms();                                     /* 再忙等约 350 ms,避免紧接着发送心跳帧。 */

Send_buf[0]=0x55;                                 /* 心跳帧第 0 字节写固定帧头。 */
Send_buf[1]=0xB;                                  /* 第 1 字节写类型码 0x0B,课程把它定义为心跳。 */
Send_buf[2]=0xB;                                  /* 第 2 字节再写一个 0x0B,使心跳也符合当前 5 字节格式。 */
Send_buf[3]=Send_buf[0]+Send_buf[1]+Send_buf[2];  /* 计算 0x55+0x0B+0x0B,低 8 位结果为 0x6B。 */
Send_buf[4]=0xAA;                                 /* 写入心跳帧固定帧尾。 */
for(i=0;i<5;i++)                                  /* 遍历心跳帧的五个字节。 */
UART_Send_Byte(Send_buf[i]);                      /* 通过串口逐字节阻塞发送心跳帧。 */
Delay350ms();                                     /* 第三次忙等约 350 ms;随后主循环才进入后续窗帘标志判断。 */

/* ==================== STM32:Gizwits/gizwits_product.c ==================== */
uint8_t zig_count=81;                             /* 定义 ZigBee 超时计数器并置为 81,使设备刚启动时先显示链路错误。 */
extern TIM_HandleTypeDef htim4;                   /* 引用 CubeMX 在 tim.c 中定义的 TIM4 句柄,供回调识别中断来源。 */

/* userHandle() 中新增的链路状态显示。 */
if(zig_count>80)                                  /* 超过 80 个定时周期仍没有心跳时,判定远端链路超时。 */
{                                                 /* 超时显示分支开始。 */
	OLED_ShowString(64,6,"Zig:ERR",16);          /* 在 OLED 最后一行右半区显示 Zig:ERR。 */
}else{                                            /* 计数值不大于 80,说明最近收到过心跳。 */
	OLED_ShowString(64,6,"Zig:OK ",16);          /* 显示 Zig:OK,末尾空格用于覆盖 ERR 遗留的字符。 */
}                                                 /* ZigBee 状态显示判断结束。 */

/* 在已有的 HAL_TIM_PeriodElapsedCallback() 中加入 TIM4 分支。 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) /* HAL 在已启动的基本定时器发生更新中断后调用此回调。 */
{                                                 /* 定时器周期回调函数开始。 */
	if(htim==&htim2)                               /* 保留工程原有的 TIM2 分支。 */
	{                                               /* TIM2 分支开始。 */
		keyHandle();                                  /* 执行机智云工程原有的按键扫描。 */
		gizTimerMs();                                 /* 维护机智云协议所需的软件毫秒计时。 */
	}                                               /* TIM2 分支结束。 */
		if(htim==&htim4)                            /* 判断本次周期中断是否来自新增的 TIM4。 */
	{                                               /* TIM4 心跳监测分支开始。 */
		zig_count++;                                  /* 每次 TIM4 更新中断把超时计数器加 1。 */
		if(zig_count>100)                             /* 计数超过 100 时进行限幅,避免 uint8_t 最终回绕到 0。 */
			zig_count=90;                              /* 把计数拉回 90;它仍大于 80,因此断线状态不会误显示为 OK。 */
	}                                               /* TIM4 心跳监测分支结束。 */
}                                                 /* 定时器周期回调函数结束。 */

/* HAL_UART_RxCpltCallback() 的 USART3 switch 中,本节接着 0x05~0x08 增加三个分支。 */
case 0x9:                                         /* 类型码 0x09 表示第 2 数据字节携带远端温度。 */
	 currentDataPoint.valuetemp2 = uart3_recv_buff[2];//Add Sensor Data Collection /* 把数据字节写入机智云“卧室温度”数据点。 */
	break;                                          /* 结束温度分支,防止继续执行后面的 case。 */
case 0xa:                                         /* 类型码 0x0A 表示第 2 数据字节携带远端湿度。 */
	currentDataPoint.valuehumi2 = uart3_recv_buff[2];//Add Sensor Data Collection /* 把数据字节写入机智云“卧室湿度”数据点。 */
	break;                                          /* 结束湿度分支。 */
case 0xb:                                         /* 类型码 0x0B 表示收到 51 发来的心跳帧。 */
	zig_count=0;                                    /* 将超时计数器清零,重新开始计算距最近一次心跳的时间。 */
	break;                                          /* 结束心跳分支。 */

源码范围说明:顶部只摘录 174 视频实际展示和编写的业务代码。51 端保留温度、湿度与心跳三类 5 字节发送;STM32 端保留数据点赋值、心跳清零、TIM4 计数和 OLED 状态显示。DHT11 驱动、HAL 库函数及 CubeMX 自动生成的整段初始化没有搬入。配套最终工程已经包含下一节调试后补齐的 TIM4/USART3 中断使能、HAL_TIM_Base_Start_IT(&htim4) 等修改;这些不是 174 当堂已经验证成功的代码,因此没有提前补到顶部。

一、五幺单片机温湿度数据返回

173 已经建立双向通信的基本框架:STM32 向 51 发送 4 字节控制帧,51 回 5 字节 ACK。本节继续复用 51→STM32 的固定 5 字节格式,增加三类主动上报:

类型码第 2 数据字节用途完整格式
0x09温度 T上报远端温度55 09 T checksum AA
0x0A湿度 H上报远端湿度55 0A H checksum AA
0x0B固定 0x0B心跳55 0B 0B 6B AA

三种帧都使用同一个 Send_buf[5],依次覆盖后发送。它们属于课程自定义的应用层协议,不是 ZigBee 标准帧格式。

1.1 五⺓单片机温湿度数据读取与传输

51 主循环先执行 DHT11_Read_Byte(&H, &T)。配套驱动的函数声明是 void DHT11_Read_Byte(int *humi, int *temp),函数内部把 DHT11 第 0 字节的整数湿度写给第一个指针,把第 2 字节的整数温度写给第二个指针,所以这里确实是 H=湿度、T=温度,不是根据变量字母猜测。

DHT11 驱动已经在前面的课程完成,本节只使用读取结果,没有重新讲解其 40 位时序,因而顶部不强行补入整个驱动。驱动会校验 DHT11 自身的五字节和校验;若失败,它把整数温湿度清零。业务层随后仍会把这个 0 发送出去,并没有额外的“本次采样无效”状态码。

51 端发出的两个传感器帧分别是:

温度:[0] 0x55  [1] 0x09  [2] T  [3] 0x55+0x09+T  [4] 0xAA
湿度:[0] 0x55  [1] 0x0A  [2] H  [3] 0x55+0x0A+H  [4] 0xAA

原笔记称第三项为 CRC 不准确。源码只是把前三个字节相加并存入一个字节,是简单累加和。它能发现一部分传输错误,但没有 CRC 所需的生成多项式等规则,也无法可靠发现所有互相抵消的多字节错误。

每帧通过五次 UART_Send_Byte() 阻塞发送,之后调用 Delay350ms()。所以不是“约一秒才发一包”:正常一轮中温度帧、湿度帧、心跳帧依次发送,相邻两帧约隔 350 ms,同类帧再次出现至少约隔三段延时,即约 1.05 s,另外还要加上传感器读取、串口发送及其他业务耗时。

1.2 三二单片机串口中断接收与结构体赋值

STM32 沿用 173 的 5 字节 USART3 接收。完整校验条件仍是:

buf[0] == 0x55
buf[0] + buf[1] + buf[2] == buf[3]
buf[4] == 0xAA

校验通过后,switch(uart3_recv_buff[1]) 按类型码分流。0x09 将 uart3_recv_buff[2] 赋给 currentDataPoint.valuetemp2,0x0A 则赋给 currentDataPoint.valuehumi2。原笔记中的“第2和第3字节赋给 time 成员”有三处问题:

  • 只取数组下标 [2] 的一个数据字节,不是同时取第 2、3 两个下标;
  • 成员名是 valuetemp2 和 valuehumi2,没有 time;
  • buf[1] 是数据类型,buf[2] 才是真实温度或湿度。

赋值发生在串口接收完成回调中,之后机智云主循环会使用更新后的 currentDataPoint 上报。课程没有为远端数据附带时间戳、采样序号或有效标志;若链路断开,结构体会保留最后一次收到的温湿度旧值,只有单独的 Zig:ERR 提示链路超时。

1.3 例题1: 三二单片机接收五⺓单片机温湿度数据

假设远端测得 T=25(0x19)、H=60(0x3C),两帧应为:

数据帧内容累加和计算
温度 25 ℃55 09 19 77 AA0x55+0x09+0x19=0x77
湿度 60%55 0A 3C 9B AA0x55+0x0A+0x3C=0x9B

STM32 收到第一帧后进入 case 0x09,把 0x19 写入 valuetemp2;收到第二帧后进入 case 0x0A,把 0x3C 写入 valuehumi2。这里的“接收正确”只表示帧头、累加和和帧尾符合课程规则,不代表传感器读数一定准确,也不代表数据一定来自预期设备,因为当前帧中没有设备地址或序号。

二、五幺单片机心跳包的发送

本节心跳帧的实际内容是:

55 0B 0B 6B AA

原笔记写成“0x55、0xAA、0x0B、CRC、0xAA”是转写错误。两个中间字节都是 0x0B:第一个是心跳类型码,第二个只是固定填充值;累加和为 0x55+0x0B+0x0B=0x6B。

之所以保留两个 0x0B,不是心跳在协议上必须重复,而是 STM32 当前用 HAL_UART_Receive_IT(..., 5) 固定收满 5 字节才回调。ACK、温度、湿度和心跳统一成 5 字节,接收端无需面对可变长度。若改用空闲线检测或“帧头+长度+载荷”的解析状态机,心跳载荷可以重新设计,但仅删除一个字节而不同时修改接收器会让后续固定 5 字节分组错位。

STM32 只有在整个 5 字节帧通过校验,并进入 case 0x0B 后才执行 zig_count=0。因此随机收到一个字节 0x0B 不会刷新心跳。这个机制监测的是“最近是否收到过一帧有效的 51 心跳”,能覆盖 51 断电、串口线断开或无线链路中断等情况,但不能证明每个传感器值都有效,也不能证明窗帘动作已经完成。

三、温湿度数据的处理

本节真正新增的“处理”分成两条路径:温湿度帧更新机智云数据点;心跳帧刷新链路存活计时。OLED 上的 Zig:OK/ERR 只代表通信新鲜度,不应解释成温湿度正常、网络云端在线或所有执行器正常。

3.1 接收数据与显示问题探讨

不能在某一瞬间“没进入串口回调”就立刻显示 ERR。51 每轮主动发送三帧,中间还有延时;绝大多数 CPU 时刻本来就没有新字节到达。如果用瞬时布尔值表示收没收到,屏幕会在两帧之间错误跳变。

课程采用超时思路:每隔固定周期增加计数,收到有效心跳就清零;只有连续多个周期都没有心跳才显示错误。这相当于给通信状态设置一个允许抖动和短暂延迟的时间窗口。

还要区分“显示值”和“数据有效期”:链路变为 ERR 时,代码只改右下角状态字符串,并没有清除 valuetemp2/valuehumi2。所以 App 或其他页面可能继续显示上次成功接收的旧数据。正式项目通常会同时保存最近更新时间,并把超时数据标成 stale(过期)。

3.2 定时器操作与变量计时

课程用 zig_count 充当“距最近一次心跳已经过去多少个 TIM4 周期”的计数器:

TIM4 每次更新中断:zig_count++
收到有效 0x0B 心跳:zig_count=0
zig_count > 80:显示 Zig:ERR
zig_count <= 80:显示 Zig:OK

这与看门狗思想相似,但不是独立硬件看门狗。真正的 IWDG/WWDG 超时通常会复位 MCU;本节只是软件计数并更新 OLED,不会复位设备。

计时任务不一定必须“一项任务独占一个硬件定时器”。多个软件超时可以共用一个周期节拍,在同一个回调或调度器中分别更新计数器。本节单独使用 TIM4 是为了演示直观,并非系统设计的硬性规则。

3.3 CubeMX中定时器的配置

视频为 TIM4 选择 Internal Clock,并填写:

参数本节值
Prescaler7199
Counter Period1000
Auto-reload preloadEnable

配套工程由 8 MHz HSE 经 PLL×9 得到 72 MHz 系统时钟;APB1 虽分频为 2,但 STM32F1 的 APB 定时器时钟在分频不为 1 时再乘 2,所以 TIM4 仍得到 72 MHz。按计数器实际除数均为“寄存器值+1”计算:

$ T=\frac{(7199+1)(1000+1)}{72,000,000}\approx 0.1001\ \text{s} $

因此课程口述的“100 ms”是近似值。若在相同时钟下要得到恰好 100 ms,ARR/Counter Period 应填 999;但顶部和正文必须忠实记录本节工程实际使用的 1000,不能擅自替课程改代码。

zig_count>80 会在计数从 0 增到 81 后进入 ERR,按实际配置约为 8.108 s,而不是原笔记所写的“80 毫秒”。

3.4 定时器回调函数与代码添加

工程已经有一个 HAL_TIM_PeriodElapsedCallback() 处理 TIM2,课程在同一个函数中追加 TIM4 判断,而不是再定义第二个同名回调。HAL 回调收到一个 TIM_HandleTypeDef *htim,通过与 &htim2、&htim4 比较来区分中断来源。

调用链应理解为:

TIM4 更新事件
→ TIM4_IRQHandler()
→ HAL_TIM_IRQHandler(&htim4)
→ HAL_TIM_PeriodElapsedCallback(&htim4)
→ zig_count++

仅仅写好回调函数并不会自动产生这条调用链;外设时钟、更新中断、NVIC 和定时器启动都必须正确配置。本节视频在编译后没有上板验证,下一节才集中发现并修正相关遗漏,因此这里不把最终工程状态倒灌成“174 已经验证完成”。

3.5 定时器初始化与变量定义

extern TIM_HandleTypeDef htim4; 只是声明:它告诉当前 C 文件“句柄在别处定义”,不分配第二份对象,也不启动定时器。真正的 htim4 由 CubeMX 在 tim.c 中定义,MX_TIM4_Init() 负责配置寄存器参数。

zig_count 初值设为 81,是一种失败安全的界面策略:上电后还没收到远端心跳时先显示 ERR,收到第一帧有效心跳后才清为 0、转为 OK。若初值设 0,系统刚启动时会在尚未证明链路正常的情况下先显示约 8 秒 OK。

这个变量同时被 TIM4 回调递增、被 USART3 回调清零、被前台 userHandle() 读取,但课程源码没有将它声明为 volatile。它属于中断与前台共享状态,优化编译下存在可见性问题;精教版保留原代码,不把修正悄悄写回源码片段。

3.6 屏幕显示逻辑与代码实现

OLED 使用 (64,6),即从右半区、最后一个 16 像素高文本行开始显示。ERR 分支写 Zig:ERR,OK 分支写 Zig:OK 。OK 后面的空格很重要:从 7 个可见字符切换到较短字符串时,它可覆盖上一状态残留的最后一个 R。

计数值对应关系如下:

zig_count显示含义
0~80Zig:OK 最近约 0~8.0 s 内收到过有效心跳
81~100Zig:ERR已超过课程设置的心跳等待窗口

屏幕刷新写在频繁调用的 userHandle() 中,即使文字没有变化也会重复写 OLED。这在课程规模下能工作,但会占用 I²C/软件时序和 CPU 时间;更完整的实现可以只在状态变化时刷新,不过本节源码没有这样优化。

3.7 代码编译与潜在问题处理

视频结尾只能确认工程“编译没有报错”。编译器能够检查符号、类型和语法,却无法证明:

  • TIM4 更新中断已经在 NVIC 中使能;
  • 定时器已使用中断模式启动;
  • USART3 接收在合适时机启动并在每帧后重新挂起;
  • ZigBee 参数、接线和供电正确;
  • 心跳真的能够周期到达。

这些都属于运行时条件。175 的开头明确说明把本节代码烧入板子后“不好使”,随后才进行调试。因此本节应记录为“协议和计数逻辑写完、编译通过、尚未完成硬件验证”,不能写成整个通信功能已经完成。

3.8 变量溢出问题与处理

zig_count 是 uint8_t,若一直执行 ++,最大值 255 后会按无符号 8 位规则回绕为 0。那会使已经断线很久的设备短暂落入 <=80,错误显示 OK。

课程用以下限幅避免回绕:

每次先加 1
若结果 >100,则立即改成 90

所以断线后它在 90~100 区间循环,始终保持大于 80。把 101 拉回 90 并不是重新开始一次真实计时,只是维持“已超时”状态。工程上也可使用饱和计数,例如达到 81 后不再增加,语义会更直接;但顶部仍保留课程原写法。

3.9 例题单片机串口接收与温湿度处理

两端完整交互可分为三类:

  1. STM32→51:4 字节 0x01~0x04 控制命令;51 校验后执行 LED 或排队窗帘动作。
  2. 51→STM32:5 字节 0x05~0x08 ACK;STM32 清除相应重发标志。
  3. 51→STM32:5 字节 0x09/0x0A/0x0B 主动上报;STM32 更新远端温湿度或刷新心跳计数。

51 主循环中的三次 Delay350ms() 是阻塞延时:延时期间主循环不能立即处理刚置位的窗帘标志,但串口中断仍可接收命令并置位。窗帘动作本身又包含约 4 秒忙等,在这段时间 51 不会执行主循环前面的温湿度和心跳发送。因此当前心跳周期并非严格实时,只是课程级的存活提示。

固定长度接收还保留 173 已分析的同步风险:丢失或插入一个字节后,STM32 可能持续按错误的 5 字节边界分组。三段延时能降低连续数据压力,却不能从协议上解决重新同步、重复帧或丢帧问题。

四、思维导图完善

视频最后提出把一个 STM32 网关扩展到三个 51 终端。课程思路是在帧中增加设备号,例如把当前 5 字节上报帧扩成:

[0] 帧头  [1] 设备号  [2] 类型码  [3] 数据  [4] 累加和  [5] 帧尾

设备号可约定 1=卧室一、2=卧室二、3=书房。但这只是协议设计草案,不是“把数组从 5 改成 6”就自动完成。至少还要同步修改:

  • 所有发送缓冲区长度、字段下标和累加和范围;
  • STM32 接收长度、校验逻辑和按设备分流的数据点;
  • 控制命令中的目标设备号,以及各 51 节点只执行发给自己的命令;
  • ACK 中的设备号,否则网关无法知道是哪一个节点确认;
  • 超时计数器,应从一个全局 zig_count 改为每个节点各自维护;
  • ZigBee 模组的网络角色、地址和透明传输配置。

课程没有给出具体 ZigBee 模组型号、网络参数和三节点实测结果,所以只能把“多对一”记录为扩展方向,不能宣称已经验证。仅有设备号也不能防止重复执行;若要提高可靠性,还应考虑长度、序号、消息类型和更可靠的校验机制。

参考资料