Zigbee串口代码(1)

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

173.8Zigbee串口代码(1)

/* ==================== STM32:Gizwits/gizwits_product.c ==================== */
uint8_t uart3_send_buff[4]; /* 定义 4 字节发送缓冲区,保存 STM32 发给 51 的控制帧。 */
uint8_t uart3_recv_buff[5]; /* 定义 5 字节接收缓冲区,保存 51 回给 STM32 的应答帧。 */
uint8_t led_on_cmd=0,led_off_cmd=0,curtain_on_cmd=0,curtain_off_cmd=0; /* 定义并清零四个待发送标志;课程源码没有使用 volatile。 */
extern UART_HandleTypeDef huart3; /* 声明 CubeMX 在 usart.c 中定义的 USART3 句柄。 */

/* gizwitsEventProcess() 的 EVENT_led1 分支中,本节在 172 已有硬件操作旁加入以下标志位逻辑。 */
if(0x01 == currentDataPoint.valueled1) /* App 下发 led1=1 时进入开灯分支。 */
{                                     /* 开灯事件分支开始。 */
	led_on_cmd=1;                      /* 把“开灯命令待确认”标志置 1,让 userHandle() 持续发送 0x01。 */
	//user handle;保留机智云模板原有的占位注释。
}                                     /* 开灯事件分支结束。 */
else                                  /* App 下发值不是 1 时进入关灯分支。 */
{                                     /* 关灯事件分支开始。 */
	led_off_cmd=1;                     /* 把“关灯命令待确认”标志置 1,让 userHandle() 持续发送 0x02。 */
	//user handle;保留机智云模板原有的占位注释。
}                                     /* 关灯事件分支结束。 */

/* gizwitsEventProcess() 的 EVENT_curtain 分支中,本节加入以下标志位逻辑。 */
if(0x01 == currentDataPoint.valuecurtain) /* App 下发 curtain=1 时进入开窗帘分支。 */
{                                        /* 开窗帘事件分支开始。 */
	curtain_on_cmd = 1;                   /* 把“开窗帘命令待确认”标志置 1,让前台循环发送 0x03。 */
	//user handle;保留生成包原有注释,真正发送动作位于 userHandle()。
}                                        /* 开窗帘事件分支结束。 */
else                                     /* App 下发值不是 1 时进入关窗帘分支。 */
{                                        /* 关窗帘事件分支开始。 */
	curtain_off_cmd = 1;                  /* 把“关窗帘命令待确认”标志置 1,让前台循环发送 0x04。 */
	//user handle;保留生成包原有注释。
}                                        /* 关窗帘事件分支结束。 */

/* userHandle():只要相应标志仍为 1,每次主循环都会重新组帧并阻塞发送一次。 */
if(led_on_cmd)                                      /* 判断“开灯命令待确认”标志是否仍为 1。 */
{                                                   /* 开灯控制帧组装开始。 */
	uart3_send_buff[0]= 0x55;                        /* 第 0 字节写固定帧头 0x55。 */
	uart3_send_buff[1]= 0x1;                         /* 第 1 字节写命令 0x01,表示打开 51 端四只 LED。 */
	uart3_send_buff[2]= uart3_send_buff[0]+uart3_send_buff[1]; /* 第 2 字节写帧头与命令的简单累加和 0x56。 */
	uart3_send_buff[3]= 0xAA;                        /* 第 3 字节写固定帧尾 0xAA。 */
	HAL_UART_Transmit(&huart3, uart3_send_buff, 4, 100); /* 通过 USART3 发送 4 字节,阻塞等待的超时参数为 100 ms。 */
}                                                   /* 开灯控制帧发送结束。 */

if(led_off_cmd)                                     /* 判断“关灯命令待确认”标志是否仍为 1。 */
{                                                   /* 关灯控制帧组装开始。 */
	uart3_send_buff[0]= 0x55;                        /* 写入固定帧头。 */
	uart3_send_buff[1]= 0x2;                         /* 写入命令 0x02,表示关闭 51 端四只 LED。 */
	uart3_send_buff[2]= uart3_send_buff[0]+uart3_send_buff[1]; /* 计算并写入简单累加和 0x57。 */
	uart3_send_buff[3]= 0xAA;                        /* 写入固定帧尾。 */
	HAL_UART_Transmit(&huart3, uart3_send_buff, 4, 100); /* 通过 USART3 阻塞发送关灯帧。 */
}                                                   /* 关灯控制帧发送结束。 */

if(curtain_on_cmd)                                  /* 判断“开窗帘命令待确认”标志是否仍为 1。 */
{                                                   /* 开窗帘控制帧组装开始。 */
	uart3_send_buff[0]= 0x55;                        /* 写入固定帧头。 */
	uart3_send_buff[1]= 0x3;                         /* 写入命令 0x03,要求 51 端执行窗帘正转。 */
	uart3_send_buff[2]= uart3_send_buff[0]+uart3_send_buff[1]; /* 计算并写入简单累加和 0x58。 */
	uart3_send_buff[3]= 0xAA;                        /* 写入固定帧尾。 */
	HAL_UART_Transmit(&huart3, uart3_send_buff, 4, 100); /* 通过 USART3 阻塞发送开窗帘帧。 */
}                                                   /* 开窗帘控制帧发送结束。 */

if(curtain_off_cmd)                                 /* 判断“关窗帘命令待确认”标志是否仍为 1。 */
{                                                   /* 关窗帘控制帧组装开始。 */
	uart3_send_buff[0]= 0x55;                        /* 写入固定帧头。 */
	uart3_send_buff[1]= 0x4;                         /* 写入命令 0x04,要求 51 端执行窗帘反转。 */
	uart3_send_buff[2]= uart3_send_buff[0]+uart3_send_buff[1]; /* 计算并写入简单累加和 0x59。 */
	uart3_send_buff[3]= 0xAA;                        /* 写入固定帧尾。 */
	HAL_UART_Transmit(&huart3, uart3_send_buff, 4, 100); /* 通过 USART3 阻塞发送关窗帘帧。 */
}                                                   /* 关窗帘控制帧发送结束。 */

/* uartInit():USART3 与 NVIC 已由 CubeMX 初始化,这里启动第一次固定 5 字节的中断接收。 */
HAL_UART_Receive_IT(&huart3, uart3_recv_buff, 5);   /* 让 HAL 从 USART3 连续收满 5 字节后调用接收完成回调。 */

/* 已存在的 HAL_UART_RxCpltCallback() 中追加 USART3 分支;不能再定义第二个同名回调函数。 */
if(UartHandle->Instance == USART3)                  /* 判断本次接收完成事件是否来自 USART3。 */
{                                                   /* USART3 回调分支开始。 */
	if(uart3_recv_buff[0]==0x55&& uart3_recv_buff[0]+uart3_recv_buff[1]+uart3_recv_buff[2]==uart3_recv_buff[3]&&uart3_recv_buff[4]==0xAA) /* 同时校验帧头、三字节累加和与帧尾。 */
	{                                                /* 5 字节应答帧校验通过。 */
		switch(uart3_recv_buff[1])                   /* 用第 1 字节的应答码选择要清零的待确认标志。 */
		{                                             /* switch 分支表开始。 */
			case 0x5:                                 /* 0x05 表示 51 已接收 0x01 开灯命令。 */
				led_on_cmd=0;                         /* 清除开灯重发标志。 */
				break;                                /* 结束 0x05 分支。 */
			case 0x6:                                 /* 0x06 表示 51 已接收 0x02 关灯命令。 */
				led_off_cmd=0;                        /* 清除关灯重发标志。 */
				break;                                /* 结束 0x06 分支。 */
			case 0x7:                                 /* 0x07 表示 51 已接收 0x03 开窗帘命令。 */
				curtain_on_cmd=0;                     /* 清除开窗帘重发标志。 */
				break;                                /* 结束 0x07 分支。 */
			case 0x8:                                 /* 0x08 表示 51 已接收 0x04 关窗帘命令。 */
				curtain_off_cmd=0;                    /* 清除关窗帘重发标志。 */
				break;                                /* 结束 0x08 分支。 */
			default:break;                            /* 其他应答码在本节没有处理。 */
		}                                             /* switch 分支表结束。 */
	}                                                /* 有效应答帧处理结束。 */
	HAL_UART_Receive_IT(&huart3, uart3_recv_buff, 5); /* 无论帧是否有效,都重新启动下一次 5 字节接收。 */
}                                                   /* USART3 回调分支结束。 */

/* ==================== 51 单片机:物联网51代码/main.c ==================== */
uchar Recv_Buf[4];                                  /* 保存 STM32 发来的 4 字节控制帧。 */
uchar Send_buf[5];                                  /* 保存 51 回给 STM32 的 5 字节应答帧。 */
char step_cw_flag=0,step_ccw_flag=0;                /* 定义并清零窗帘正转、反转请求标志。 */
uchar phasecw[4] ={0x08,0x04,0x02,0x01};           /* 保存步进电机正向四拍输出序列。 */
uchar phaseccw[4]={0x01,0x02,0x04,0x08};           /* 保存步进电机反向四拍输出序列。 */

/* main() 的 while(1) 中:耗时的电机动作放在前台执行,而不是直接塞进串口中断。 */
if(step_cw_flag)                                    /* 判断串口中断是否置位了正转请求。 */
{                                                   /* 窗帘正转业务开始。 */
	sprintf(OLED_Buf,"Curtain:ON ",H,T);            /* 依照课程原代码生成窗帘打开文本;多传的 H、T 参数没有被格式串使用。 */
	OLED_ShowString(0,6,OLED_Buf,16);                /* 在 OLED 最后一行显示 Curtain:ON。 */
	for(y=0;y<200;y++)                               /* 外层循环执行 200 组四拍序列。 */
	{                                                /* 外层电机循环开始。 */
		for(i=0;i<4;i++)                              /* 依次输出正向序列的四个相位。 */
		{                                             /* 单组四拍循环开始。 */
			P2 = phasecw[i];                           /* 把当前正向相位码写到 51 单片机 P2 口。 */
			Delay5ms();                                /* 每一拍保持约 5 ms。 */
		}                                             /* 单组四拍循环结束。 */
	}                                                /* 200 组正向序列结束。 */
	step_cw_flag=0;                                  /* 电机动作完成后清除 51 本地正转标志。 */
}                                                   /* 窗帘正转业务结束。 */

if(step_ccw_flag)                                   /* 判断串口中断是否置位了反转请求。 */
{                                                   /* 窗帘反转业务开始。 */
	sprintf(OLED_Buf,"Curtain:OFF",H,T);            /* 依照课程原代码生成窗帘关闭文本。 */
	OLED_ShowString(0,6,OLED_Buf,16);                /* 在 OLED 最后一行显示 Curtain:OFF。 */
	for(y=0;y<200;y++)                               /* 外层循环执行 200 组反向四拍序列。 */
	{                                                /* 外层电机循环开始。 */
		for(i=0;i<4;i++)                              /* 依次输出反向序列的四个相位。 */
		{                                             /* 单组四拍循环开始。 */
			P2 = phaseccw[i];                          /* 把当前反向相位码写到 P2 口。 */
			Delay5ms();                                /* 每一拍保持约 5 ms。 */
		}                                             /* 单组四拍循环结束。 */
	}                                                /* 200 组反向序列结束。 */
	step_ccw_flag=0;                                 /* 电机动作完成后清除 51 本地反转标志。 */
}                                                   /* 窗帘反转业务结束。 */

/* 51 串口中断:课程源码使用 Keil C51 的 interrupt 4 扩展语法,函数原文没有显式返回类型。 */
UART_Routine(void) interrupt 4                      /* 定义串口中断服务函数;interrupt 4 是 8051 串口中断向量号。 */
{                                                   /* 51 串口中断函数开始。 */
	static int i = 0;  //static;静态索引在多次中断之间保留当前接收位置。
	int x=0;                                          /* 定义发送循环和清缓冲区所用的局部索引。 */
	char Recv;                                        /* 保存本次从串口数据寄存器读出的一个字节。 */
	if(1 == RI) //if(RI);判断串口接收中断标志是否置位。
	{                                                 /* 收到一个字节后的处理开始。 */
		RI=0;                                         /* 软件清除接收中断标志,准备接收后续字节。 */
		Recv=SBUF;                                    /* 从串口缓冲寄存器取出本次收到的字节。 */
		if(Recv == 0X55)                              /* 任意收到 0x55 时都把它当成新帧帧头。 */
		{                                             /* 帧头分支开始。 */
			i = 0;                                     /* 把接收下标重新定位到第 0 项。 */
			Recv_Buf[i] = Recv;                        /* 将 0x55 保存为控制帧第 0 字节。 */
		}else{                                        /* 当前字节不是 0x55。 */
			i++;                                       /* 接收下标先递增到下一项。 */
			Recv_Buf[i] = Recv;                        /* 保存当前字节;课程源码在边界检查前写入,存在越界风险。 */
		}                                             /* 本次字节入缓冲结束。 */

		if(0x55==Recv_Buf[0]&&Recv_Buf[0]+Recv_Buf[1]==Recv_Buf[2]&&Recv_Buf[3]==0xAA) /* 校验 4 字节控制帧的头、累加和与尾。 */
		{                                             /* 控制帧校验通过。 */
			switch(Recv_Buf[1])                       /* 按第 1 字节的控制命令选择动作。 */
			{                                         /* 51 命令分支表开始。 */
				case 0x1:                              /* 0x01:打开 51 开发板四只低电平有效 LED。 */
					LED1=0;LED2=0;LED3=0;LED4=0;       /* 将 P1.0~P1.3 全部拉低,点亮四只 LED。 */
					Send_buf[0]=0x55;                   /* 组装 5 字节应答帧的帧头。 */
					Send_buf[1]=Recv_Buf[1]+0x4;       /* 把命令 0x01 加 4,生成应答码 0x05。 */
					Send_buf[2]=Recv_Buf[1]+0x4;       /* 再复制一份 0x05 作为第 2 数据字节。 */
					Send_buf[3]=Send_buf[0]+Send_buf[1]+Send_buf[2]; /* 计算三个字节的简单累加和 0x5F。 */
					Send_buf[4]=0xAA;                  /* 写入应答帧固定帧尾。 */
					for(x=0;x<5;x++)                   /* 依次遍历应答帧的五个字节。 */
						UART_Send_Byte(Send_buf[x]);     /* 在中断中逐字节阻塞发送开灯应答。 */
					break;                              /* 结束 0x01 命令分支。 */
				case 0x2:                              /* 0x02:关闭 51 开发板四只 LED。 */
					P1|=0x0F;                           /* 将 P1 低四位置 1;因 LED 低电平有效,所以这是关灯。 */
					Send_buf[0]=0x55;                   /* 写入应答帧头。 */
					Send_buf[1]=Recv_Buf[1]+0x4;       /* 生成应答码 0x06。 */
					Send_buf[2]=Recv_Buf[1]+0x4;       /* 再复制一份 0x06。 */
					Send_buf[3]=Send_buf[0]+Send_buf[1]+Send_buf[2]; /* 计算累加和 0x61。 */
					Send_buf[4]=0xAA;                  /* 写入固定帧尾。 */
					for(x=0;x<5;x++)                   /* 遍历五个应答字节。 */
						UART_Send_Byte(Send_buf[x]);     /* 在中断中逐字节发送关灯应答。 */
					break;                              /* 结束 0x02 命令分支。 */
				case 0x3:                              /* 0x03:请求窗帘正转。 */
					step_cw_flag=1;                     /* 只置位标志,把耗时的电机动作留给主循环。 */
					Send_buf[0]=0x55;                   /* 写入应答帧头。 */
					Send_buf[1]=Recv_Buf[1]+0x4;       /* 生成应答码 0x07。 */
					Send_buf[2]=Recv_Buf[1]+0x4;       /* 再复制一份 0x07。 */
					Send_buf[3]=Send_buf[0]+Send_buf[1]+Send_buf[2]; /* 计算累加和 0x63。 */
					Send_buf[4]=0xAA;                  /* 写入固定帧尾。 */
					for(x=0;x<5;x++)                   /* 遍历五个应答字节。 */
						UART_Send_Byte(Send_buf[x]);     /* 立即发送“已接收开窗帘命令”的应答。 */
					break;                              /* 结束 0x03 命令分支。 */
				case 0x4:                              /* 0x04:请求窗帘反转。 */
					step_ccw_flag=1;                    /* 只置位标志,把电机反转留给主循环。 */
					Send_buf[0]=0x55;                   /* 写入应答帧头。 */
					Send_buf[1]=Recv_Buf[1]+0x4;       /* 生成应答码 0x08。 */
					Send_buf[2]=Recv_Buf[1]+0x4;       /* 再复制一份 0x08。 */
					Send_buf[3]=Send_buf[0]+Send_buf[1]+Send_buf[2]; /* 计算累加和 0x65。 */
					Send_buf[4]=0xAA;                  /* 写入固定帧尾。 */
					for(x=0;x<5;x++)                   /* 遍历五个应答字节。 */
						UART_Send_Byte(Send_buf[x]);     /* 立即发送“已接收关窗帘命令”的应答。 */
					break;                              /* 结束 0x04 命令分支。 */
				default:break;                         /* 其他命令在本节不处理。 */
			}                                         /* 51 命令分支表结束。 */
			for(x=0;x<4;x++)                         /* 遍历 4 字节接收缓冲区。 */
				Recv_Buf[x]=0;                        /* 有效帧处理后逐项清零接收缓冲区。 */
		}                                             /* 有效控制帧处理结束。 */
		if(i>4)                                      /* 课程原代码在 i 大于 4 后才复位索引。 */
			i=0;                                      /* 重置接收索引;此检查晚于前面的数组写入。 */
	}                                                 /* 接收中断标志处理结束。 */
}                                                   /* 51 串口中断函数结束。 */

源码范围说明:顶部按 173 视频展示和编写的边界,分别摘录 STM32 的命令标志、4 字节发送、5 字节中断应答接收,以及 51 的控制帧解析、应答和窗帘标志执行代码。片段来自两个独立工程,不是要拼成一个可编译文件。51 工程中已经存在但留到 174 才讲解的温湿度与心跳发送,STM32 最终工程中的 case 0x09/0x0A/0x0B、zig_count 和 TIM4 逻辑均未提前补入。CubeMX 自动生成的 USART/NVIC 初始化和 HAL 库函数实现也没有冒充本节业务源码。

一、简易温湿度物联网远程检测项目

本节建立的是两种长度、两个方向的应用层帧协议:

手机 App ─► 机智云 ─► STM32 EVENT
                         │
                         │ USART3:4 字节控制帧
                         ▼
                    本端 ZigBee ))) 无线 ((( 远端 ZigBee
                                                   │ UART
                                                   ▼
                                              51 单片机
                                                   │
                         5 字节应答帧              │ 执行 LED / 排队窗帘动作
                    STM32 ◄────────────────────────┘

ZigBee 模组在本项目中承担无线串口透传;0x55/命令/校验/0xAA 是课程自行定义在透明字节流之上的应用协议,并不是 ZigBee 标准帧格式。

1.1 上节回顾

172 已完成 STM32 本地传感器、蜂鸣器、第一路 LED 和 OLED。173 计划完成两项远端通信:

  1. STM32 接到 App 的开灯、关灯、开窗帘、关窗帘事件后,通过 USART3/ZigBee 向 51 发送命令,并持续重发到收到应答。
  2. 51 定期向 STM32 发送心跳;STM32 超时后在 OLED 显示 ZigBee 错误。

本节真正完成的是第 1 项的基本代码。温湿度回传和心跳仅在开头说明目标,视频结尾明确说“还剩温湿度数据”,它们在 174 才实现。因此不能用后续最终工程的心跳代码制造“本节已经全部完成”的假象。

这里的“应答”还要准确理解:51 对 LED 命令是先改变端口再回复;对窗帘命令则是在串口中断中置位 step_*_flag 后立即回复,电机动作随后才在主循环执行。因此 0x07/0x08 表示“命令已通过校验并被接收/排队”,并不证明窗帘已运动到位。

1.2 五⺓单片机代码

标题中的“五⺓/五幺/五一”均指课程的 51 单片机节点。该节点使用串口中断逐字节接收 STM32 的控制帧:灯可以在中断中快速切换;步进电机动作耗时较长,只在中断里置标志,真正转动放在 while(1)。

51 工程的 UART_Init() 配置串口模式 1、允许接收、启用串口中断,并用定时器 1 模式 2产生波特率。课程板晶振是 11.0592 MHz,TH1=TL1=0xFD、SMOD=0 对应约 9600 bit/s,与 STM32 USART3 的 9600、8 数据位、无校验、1 停止位相匹配。

1.2.1 五⺓单片机接收代码: 串口中断与数据包接收

8051 的接收过程不是“一次中断自动得到一帧”,而是每收到一个字节触发一次串口中断。课程用静态索引 i 把字节依次放入 Recv_Buf[4]:

第 1 次中断:0x55 → Recv_Buf[0]
第 2 次中断:命令 → Recv_Buf[1]
第 3 次中断:累加和 → Recv_Buf[2]
第 4 次中断:0xAA → Recv_Buf[3] → 校验并执行

RI 是 8051 串口接收中断标志,硬件收到字节后置 1;中断函数先将它清 0,再从 SBUF 取出字节。发送与接收共用串口中断向量 4,课程代码只显式处理 RI;发送函数则轮询 TI 等待每个字节发送完毕。

课程接收器存在明确的数组越界风险:Recv_Buf 只有 4 项,合法下标是 0~3,但源码先执行 i++ 和 Recv_Buf[i]=Recv,之后才在 i>4 时复位。若字节流丢失帧头或多出字节,i=4、i=5 时已经写到数组外。精教版保留原代码,但不能把它视为健壮的串口解析模板。

1.2.2 五⺓单片机接收代码: 数据包构成

STM32 发给 51 的控制帧固定为 4 字节,而不是原笔记误写的 5~6 字节:

下标含义示例:开窗帘
[0]帧头0x55
[1]命令0x03
[2]简单累加和 [0]+[1]0x58
[3]帧尾0xAA

命令定义为:

命令51 端动作
0x01LED1~LED4=0,四只低电平有效 LED 全亮
0x02`P1
0x03step_cw_flag=1,请求窗帘正转
0x04step_ccw_flag=1,请求窗帘反转

原笔记把 P1 |= 0x0F 说成开灯不准确。51 开发板原理图中 LED 阳极侧接 +5 V,阴极经电阻接 P1.0~P1.3,因此端口写 0 才点亮,写 1 熄灭。

1.2.3 五⺓单片机接收代码: 数据包校验

课程口述把第 3 字节称为“CRC”,但源码只计算:

$ checksum = header + command $

这属于简单累加校验,不是 CRC。CRC 需要约定生成多项式、初值、输入/输出反射和最终异或等规则,并按位或查表计算。累加校验能发现一部分单字节错误,但无法可靠发现所有多字节抵消错误,也没有序号、长度或设备地址。

0x55 和 0xAA 只负责帮助识别边界,不能保证中间数据正确;三项校验要同时通过:

Recv_Buf[0] == 0x55
Recv_Buf[0] + Recv_Buf[1] == Recv_Buf[2]
Recv_Buf[3] == 0xAA

源码使用的是逻辑与 &&,不是按位与 &。逻辑与会按从左到右短路判断,三项均为真时才执行 switch。

1.2.4 三二单片机发送代码: 数据包发送流程

STM32 的发送流程是“填满同一个 4 字节数组,再调用一次 HAL 发送”:

[0] 0x55 → [1] 命令 0x01~0x04 → [2] [0]+[1] → [3] 0xAA

原笔记中“再发送地址 0x32、再发送 CRC、总共六字节”并未出现在视频或源码中,应删除。HAL_UART_Transmit(&huart3, ..., 4, 100) 的 4 才是本方向实际长度,100 是阻塞发送的超时参数,不是帧间延时。

1.3 三二单片机代码

STM32 USART3 的实际配置为:

项目配置
TXPB10
RXPB11
波特率9600 bit/s
帧格式8-N-1
模式收发
接收方式中断,每次固定接收 5 字节

接线必须交叉并共地:STM32 PB10/TX 接本端 ZigBee RX,PB11/RX 接本端 ZigBee TX;另一端 ZigBee 再接 51 的 P3.0/RX、P3.1/TX。无线两端还要使用兼容的信道、PAN/网络参数和透明传输配置。

原理图能确认 STM32 的 PB10/PB11 和 51 的 P3.0/P3.1 引脚,但课程没有提供 ZigBee 模组载板的电源与电平转换原理图。51 板工作在 5 V、STM32 工作在 3.3 V,不能凭“TTL 串口”四个字断定所有模组引脚都可直接承受 5 V;应核对具体模组或转接板的供电与 UART 电平规格。

1.4 三二单片机发送部分代码

四种控制帧的确切内容如下:

App 事件STM32 标志发送帧51 动作
led1 开led_on_cmd55 01 56 AA四灯全亮
led1 关led_off_cmd55 02 57 AA四灯全灭
curtain 开curtain_on_cmd55 03 58 AA请求正转
curtain 关curtain_off_cmd55 04 59 AA请求反转

机智云的 led2 被错误配置成只读,因此没有生成 EVENT_led2。课程暂时让一个 led1 App 开关同时控制 STM32 本地 PB5 灯,并向 51 发送四灯全开/全关命令;这只是功能复用,不是“客厅与卧室两个数据点都已经正确建模”。

四个 if 相互独立,并非 else if。如果 App 在上一条命令尚未确认时快速反向切换,例如 led_on_cmd 与 led_off_cmd 同时为 1,主循环会先后发送开灯和关灯两帧,远端最终状态取决于接收与确认顺序。源码没有互斥、队列或“最新命令覆盖旧命令”策略。

1.5 五⺓单片机接收部分代码

51 校验控制帧后,立即按 Recv_Buf[1] 执行动作或置位标志,并构造一个 5 字节应答。原笔记中的命令表出现重复和错位,正确映射是 1=开灯、2=关灯、3=开窗帘、4=关窗帘。

在 51 端,灯动作直接在中断里执行;步进电机不能在中断里运行约 4 秒的双层延时循环,所以只置 step_cw_flag 或 step_ccw_flag。这种“中断只做短任务、耗时动作交给主循环”的方向是正确的,不过课程仍在中断里阻塞发送 5 个应答字节,会延长中断占用时间。

1.6 问题提出

若 STM32 只在 App 事件回调中发送一次,帧可能因无线干扰、串口拥塞或节点暂时忙碌而丢失,STM32 也无法知道 51 是否收到。本节用“待确认标志 + 重复发送 + 应答清零”建立最小交互:

App 事件 → flag=1
              ↓
userHandle 每轮检测 flag
              ↓
          重复发送命令
              ↓
      51 校验并回复 5~8
              ↓
STM32 中断收到应答 → flag=0 → 停止重发

这比只发一次更可靠,但并不等于完整可靠协议;它仍缺少重试间隔、最大次数、超时错误、序列号和重复命令去重。

1.6.1 五一单片机的接收部分

以命令 55 03 58 AA 为例:51 收到并校验通过后进入 case 0x03,把 step_cw_flag 置 1,并构造 55 07 07 63 AA 回给 STM32。转录中的“00 k4 3k4 0和3”是对十六进制代码的错误识别。

51 的串口接收器把任何 0x55 都当新帧头。当前控制命令与校验字节范围很小,不会碰巧等于 0x55 以外的帧头位置;若将协议推广到任意二进制数据,则必须考虑转义、长度字段或状态机,否则载荷中出现 0x55 会破坏同步。

1.6.2 步进电机操作与标志位变化

step_cw_flag 和 step_ccw_flag 把“接收命令”与“执行耗时电机动作”分开:

  1. 串口 ISR 校验命令并把相应标志置 1。
  2. ISR 立即发送应答后退出。
  3. 主循环检测标志,更新 OLED,输出 200 组四拍相位,每拍延时约 5 ms。
  4. 动作完成后,51 在本地把标志清 0,避免每轮继续转动。

按源码循环估算,单次动作仅显式延时就约为:

$ 200\times4\times5\ \text{ms}=4000\ \text{ms} $

这约 4 秒期间 51 主循环被阻塞,但串口中断仍可发生。课程没有位置传感器或限位开关,Curtain:ON/OFF 表示按预定步数运行后的软件状态,并不能证明机械窗帘真的到达端点。

1.6.3 0x3指令接收与步进电机正转

0x03 只负责将 step_cw_flag=1,而不是在 ISR 内直接转电机。主循环使用 phasecw={0x08,0x04,0x02,0x01} 依次驱动 P2;0x04 则使用逆序 phaseccw。

如果同一个 0x03 因应答丢失而被 STM32 重发,51 没有序列号或去重表:重发发生在第一次动作完成并清零之后时,窗帘可能再次正转。这说明“至少送达一次”不等于“动作恰好执行一次”。对电机等非幂等动作,可靠协议通常还要带事务序号并缓存最近已处理命令。

1.6.4 指令接收与回复机制

51 的应答固定为 5 字节:

原命令应答码完整应答帧课程含义
0x010x0555 05 05 5F AA已接收开灯命令
0x020x0655 06 06 61 AA已接收关灯命令
0x030x0755 07 07 63 AA已接收并排队开窗帘命令
0x040x0855 08 08 65 AA已接收并排队关窗帘命令

应答第 1 字节由“原命令 + 4”得到,第 2 字节又重复一次同样的值,第 3 字节为 [0]+[1]+[2]。之所以采用 5 字节,是为了让 STM32 用固定长度中断接收;这不是某种通用 ZigBee 应答格式。

对 0x03/0x04,51 在置位后就发送 ACK,早于电机执行。因此课程口述的“已接收并执行完毕”与实际源代码不完全一致;准确语义是“已接收并接受处理”。

1.6.5 例题1: 三二单片机交互机制问题

机智云事件分支只在 App 下发时运行一次。如果把 HAL_UART_Transmit() 只写在那里,就只能发送一次,也无法持续等待应答。本节把事件分支改成只置标志,再由持续运行的 userHandle() 负责重发。

但“持续重发直到 ACK”没有失败终点:51 断电或 ZigBee 链路断开时,标志会永久为 1,主循环每轮都进行阻塞发送。实际工程应加入重试周期、最大次数和错误状态,并避免在无间隔情况下淹没串口与无线链路。

1.7 三二单片机接收部分代码

STM32 与 51 的接收方式不同:51 每字节中断后自己拼 4 字节帧;STM32 使用 HAL 的固定长度中断接口,先声明 5 字节数组,收满后一次进入 HAL_UART_RxCpltCallback()。

1.7.1 代码文件概览与标志位初始化

本节四个标志在定义时初始化为 0,避免上电后随机发送控制帧。ad_value 是全局变量,C 语言静态存储期对象本来也会被清零,但它很快被 ADC 采样覆盖;标志位的显式 =0 更直观地表达初始“无待发送命令”。

这些标志同时在前台 userHandle() 中读取、在串口中断回调中清零,却没有声明为 volatile。在优化编译下,编译器可能缓存前台看到的值,使中断修改不能按预期被重新读取。课程源码必须原样记录,但工程代码应把这种 ISR/前台共享状态按并发规则处理,至少正确使用 volatile,更复杂状态还需要临界区或原子操作。

1.7.2 按键对应功能与指令发送

这里的“按键”是 App 中的虚拟开关事件,不是板上实体按键。EVENT_led1 和 EVENT_curtain 分别把 on/off 标志置 1;事件分支本身不等待 51,也不直接清除标志。

开、关标志彼此独立。源码没有在 led_on_cmd=1 时主动清 led_off_cmd,窗帘也同样;快速反向操作可能让相反命令同时处于待确认状态。

1.7.3 传感器数据收集与指令发送

四段发送逻辑放在 userHandle() 的数据采集代码之后。每个标志为真时,都重新写 uart3_send_buff 并调用 HAL_UART_Transmit()。转录中的“Ctrl+X-7”等只是编辑操作识别错误,不是协议或快捷键要求。

HAL_UART_Transmit() 返回值被忽略。若发生超时或 HAL 状态忙,程序不会记录错误;因为标志仍是 1,下一轮仍会继续尝试,这形成一种非常粗糙的重试。

1.7.4 初始化与user handle函数

userHandle() 不是 while(1) 的语法组成部分,而是 main.c 在无限循环中主动调用的普通函数:

while (1)                                      /* MCU 前台主循环持续运行。 */
{                                              /* 一轮循环开始。 */
    userHandle();                              /* 采集本地数据并按标志发送远端命令。 */
    gizwitsHandle((dataPoint_t *)&currentDataPoint); /* 继续处理机智云协议。 */
}                                              /* 返回循环顶部,开始下一轮。 */

因此事件把标志置 1 后,后续每轮都会进入相应 if。这就是课程口述“像 while(1) 一样一直运行”的真实调用关系。

1.7.5 例题1:userHandle函数中的数据收集与处理

只要没有收到匹配 ACK,标志保持 1,控制帧就会重复发送。四帧每次使用同一全局缓冲区,但发送接口是阻塞式:一次调用返回后才进入下一个 if,所以当前前台内不会同时改写正在发送的数组。

不过代码没有重试间隔,实际循环中 DHT11 阻塞和其他业务偶然限制了频率,不能把这种副作用当作通信节流设计。可靠实现应明确规定“多久重发一次、最多几次、超时后向 App/OLED 报什么错误”。

1.7.6 标志位清零与串口接收需求

标志清零发生在 STM32 收到并校验通过 5 字节应答之后:5 清 led_on_cmd、6 清 led_off_cmd、7 清 curtain_on_cmd、8 清 curtain_off_cmd。

原笔记把方向写反为“51 发送指令→32 清零→32 等待返回”。准确时序是:

STM32 发控制命令 → 51 接收并回复 ACK → STM32 接收 ACK 并清零标志
1.7.7 串口中断接收与初始化

轮询也能接收串口,不是“只有中断才能接收”;课程选择中断,是为了让数据到达时由硬件及时通知 HAL,避免前台同时执行 DHT11、ADC、机智云和多路发送时长期不查看 RX。

开启中断接收需要两层都存在:

  1. CubeMX/NVIC 启用 USART3 global interrupt,生成 USART3_IRQHandler() 并调用 HAL_UART_IRQHandler(&huart3)。
  2. 应用调用 HAL_UART_Receive_IT(&huart3, uart3_recv_buff, 5),把缓冲区和期望长度交给 HAL。

只勾 NVIC 不调用 Receive_IT,HAL 没有已启动的接收任务;只调用 Receive_IT 却未启用 IRQ,也不会正常进入中断链路。

1.8 中断初始化

视频在 CubeMX 的 USART3 → NVIC Settings 中勾选 USART3 global interrupt,然后重新 Generate Code/Reload。原笔记中的“NRC”应为 NVIC,与 RT-Thread 无关。

生成结果包括:

  • HAL_NVIC_SetPriority(USART3_IRQn, 0, 0);
  • HAL_NVIC_EnableIRQ(USART3_IRQn);
  • USART3_IRQHandler() 内调用 HAL_UART_IRQHandler(&huart3)。

当前工程把 USART3 中断设为抢占优先级 0、子优先级 0。它与其他中断的优先级关系应结合整个工程统一规划;“数值 0”通常代表很高的抢占优先级,不是“没有优先级”。

1.9 重新写回调函数

HAL UART 驱动只允许应用提供一个全局 HAL_UART_RxCpltCallback() 实现。机智云工程已经在 gizwits_product.c 中用它处理 USART2 的 ESP8266 接收,因此不能在另一个文件再定义同名函数,否则会重复定义或覆盖设计。

课程的正确做法是在已有回调内按 UartHandle->Instance 分流:

USART2 完成 1 字节接收 → 交给 gizPutData() → 重启 USART2 接收
USART3 完成 5 字节接收 → 校验课程帧 → 清控制标志 → 重启 USART3 接收

stm32f1xx_it.c 中的 USART3_IRQHandler() 是硬件中断入口;HAL_UART_RxCpltCallback() 是 HAL 在收满期望字节数后调用的用户回调。二者层级不同,不能把回调直接等同于底层 IRQHandler。

1.10 中断接收开始

转录中的“五个帧”应为“五个字节”。HAL_UART_Receive_IT(..., 5) 表示收满 5 个字节才回调一次,而不是接收五帧。

huart3 在 usart.c 中定义,所以 gizwits_product.c 通过 extern UART_HandleTypeDef huart3; 引用它;也可以包含提供该声明的 usart.h,但不能在本文件再定义一个新句柄。

课程在 uartInit() 中同时启动 USART2 的 1 字节接收和 USART3 的 5 字节接收。每次回调后必须再次调用 HAL_UART_Receive_IT(),因为普通中断接收完成后不会自动永久循环。

固定 5 字节方式简单,但一旦丢失或插入一个字节,后续分组会错位。当前代码发现校验失败后只是丢弃整组并再收下一组 5 字节,没有逐字节寻找 0x55 的重新同步机制,因此可能连续丢帧。

1.11 中断处理数据

STM32 对 5 字节应答的校验为:

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

三项用逻辑与 && 连接。校验通过后只根据 buf[1] 选择 5~8,buf[2] 仅参与累加和,没有再次核对它必须等于 buf[1];不过 51 的当前发送代码确实把二者写成同一值。

由于 C 的整数提升,三个 uint8_t 相加可能得到大于 255 的 int,而校验字节只能保存低 8 位。当前 5~8 应答的和都小于 256,所以不会暴露;若协议扩展到更大数据,应明确规定模 256 并在发送、接收两端一致转换。

1.12 五⺓单片机回复处理

case 0x07 清除 curtain_on_cmd 后,STM32 停止继续发送 0x03;其他应答同理。break 防止继续落入下一 case,赋值语句后的分号是 C 语法结束符,不是协议内容。

应答本身也可能丢失。若 51 已经收到并执行/排队命令,但 ACK 在无线链路中丢失,STM32 会重发;51 没有识别重复请求,因此 LED 会重复写相同状态,窗帘却可能重复运动。该协议只能演示基本确认,不具备“恰好一次执行”保证。

1.13 开关灯指令的书写

本节最后把同一套标志、发送帧和应答清零逻辑扩展到四种命令。代码的规律一致,但复制粘贴时命令值、标志名和应答码必须成组修改,不能只改其中一个。

1.13.1 开关灯指令的复制与对⻬

视频中的复制、剪切、缩进只是编辑过程。真正需要检查的是每一组逻辑的四个对应关系:

App 事件分支置哪个 flag
→ userHandle 检测哪个 flag
→ 发送哪个命令字节
→ callback 收到哪个 ACK 后清哪个 flag

任何一处粘贴错名都会导致一直重发、清错命令或远端执行错误动作。

1.13.2 开关灯指令的介绍

原笔记把 5~8 的含义写乱。它们全部是 51 回给 STM32 的应答码,不是新的执行命令:

控制命令应答码
开灯 15
关灯 26
开窗帘 37
关窗帘 48
1.13.3 窗帘开关指令的完善

开窗帘使用 curtain_on_cmd 与命令 0x03,关窗帘复制后必须同时改成 curtain_off_cmd 与 0x04;对应 ACK 则分别是 0x07、0x08。其余帧头、帧尾和累加和规则不变。

1.13.4 灯开关指令的完善

开灯帧的命令字节是 0x01,校验为 0x55+0x01=0x56;关灯为 0x02,校验为 0x57。原笔记中“两个指令相加得到 3/4”是转写误解,第三字节始终是帧头 0x55 与当前命令相加,不是把开、关命令互相相加。

1.13.5 按键指令的完善

课程只有一个可写 led1 事件,于是将它同时用于 STM32 本地灯和 51 远端四灯;这不是把“客厅实体按键改成卧室按键”,而是复用同一个 App 数据点控制两端。

至此完成的是灯和窗帘控制命令的发送、接收与基础 ACK 停止重发。51 向 STM32 回传的温度、湿度以及心跳仍未处理,视频明确留到下一课。

参考资料

  • 本地课程配套 STM32F103ZE 开发板原理图:核对 PB10/PB11、3.3 V 供电与扩展排针连接。
  • 本地课程配套 51 开发板电路图:核对 P3.0/RX、P3.1/TX、11.0592 MHz 晶振和 P1.0~P1.3 低电平点亮 LED。
  • 本节 STM32 工程的 gizwits_product.c、usart.c、stm32f1xx_it.c 与 STM32F103C8x.ioc:核对 USART3 参数、中断链路、4/5 字节缓冲区和最终代码中的后续增量。
  • 本节 51 工程的 main.c、uart.c 与 main.h:核对控制命令、应答帧、串口初始化、LED 引脚和窗帘标志;其中温湿度/心跳源码属于 174 边界,未提前抄入顶部。