Zigbee串口代码(2)
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 AA | 0x55+0x09+0x19=0x77 |
| 湿度 60% | 55 0A 3C 9B AA | 0x55+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,并填写:
| 参数 | 本节值 |
|---|---|
| Prescaler | 7199 |
| Counter Period | 1000 |
| Auto-reload preload | Enable |
配套工程由 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~80 | Zig:OK | 最近约 0~8.0 s 内收到过有效心跳 |
81~100 | Zig: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 例题单片机串口接收与温湿度处理
两端完整交互可分为三类:
- STM32→51:4 字节
0x01~0x04控制命令;51 校验后执行 LED 或排队窗帘动作。 - 51→STM32:5 字节
0x05~0x08ACK;STM32 清除相应重发标志。 - 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 模组型号、网络参数和三节点实测结果,所以只能把“多对一”记录为扩展方向,不能宣称已经验证。仅有设备号也不能防止重复执行;若要提高可靠性,还应考虑长度、序号、消息类型和更可靠的校验机制。
参考资料
- 本节 51 工程的 main.c、dht11.c 与 delay.c:核对 H/T 参数顺序、三类发送帧、DHT11 和校验处理以及 350 ms 忙等。
- 本节 STM32 工程的 gizwits_product.c、tim.c、main.c、stm32f1xx_it.c 与 STM32F103C8x.ioc:核对数据点赋值、心跳计数、TIM4 参数和最终工程中的后续调试修改。