51单片机之串口通信

news2025/1/21 22:07:09

目录

1.串口简介

1.1TXD和RXD

1.2通讯接口

1.3通信方式

1.4 51单片机的UART模式

2.串口配置

2.1寄存器简介

        SCON寄存器配置

        PCON配置

2.2代码配置串口

2.2.1  配置串口发送数据

2.2.2配置电脑向单片机发送数据点亮LED


1.串口简介

        串口是一个应用十分广泛的通讯接口,它可以实现两个设备之间的相互通信,大大提高了单片机的硬件实力。前面讲述中断的时候顺便提过51单片机的串口中断,我们使用的51单片机使用的是UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可以实现单片机的串口通信。

1.1TXD和RXD

        简单的两个设备之间的通信可以简化成下面的图:

        VCC与GND的连接就不多说, 主要是为了供电,TXD和RXD需要解释一下:TXD可以展开成Transmit Exchange Data,即传输数据端;RXD可以展开成Receive Exchange Data,即接收数据端。也就是我们使用TXD输出数据,RXD接收数据,实现两个设备之间的相互通信。

1.2通讯接口

        就像学校机房老式台式机就有接口这样一个东西:这就是我们所说的接口

        我们设备之间的通信也是通过接口进行的,像这里列举出的常见的接口:

        UART就是我们单片机上的通讯接口,I2C也是一个常见的接口,未来会接触很多。

        此外,设备之间通信还有CANUSB.USB就是我们常用的一个简单易用的通讯工具,就像我们使用的U盘,USB数据线,很多都是USB通讯使用的。

1.3通信方式

        上面表格里写了通信接口的通信方式,这里介绍一下这个通信方式的意思:

  • 全双工:通信双方可以在同一时刻互相传输数据
  • 半双工:通信双方可以互相传输数据,但必须分时复用一根数据线
  • 单工:通信只能有一方发送到另一方,不能反向传输

1.全双工就类似有两根线 ,像这样:

        因为有两个可以相互通信的通道,所以就可以进行两个设备之间的通信,并且可以在同一时刻进行相互通信

2.半双工就是类似只有一根线,但是却又可以进行两端的通信

        因为只有一个通信的通道,所以两设备之间的通信不能在同一时间完成,需要一次进行一个单方向的通信,完成后才可以进行下一个通信。

3.单工就比较简单了,就是只能进行单方向的通信

        就好比我们的电视机遥控器向电视发送信息,这就是单工。


  • 异步:通信双方各自约定通信速率
  • 同步:通信双方靠一根时钟线来约定通信速率

1.异步就是通信双方互相约定一个速率,让两边设备虽然运作方式不同,但是可以通过约定的规则进行采样获得数据

        比如这里就是对同一个数据不同的采样方式,前一个得到的是1100,后一个得到的是10,再把这个数据变得更加复杂一点,我们使用不同的采样方式,得到的数据会完全不同,所以我们使用异步约定速率还是很重要的,并且一般异步的两个设备是可以不使用线连接的。  

2.同步是双方使用一根时钟线直接约定速率,所以一般同步都是使用线连接的。这里就不多讲


  • 总线:连接各个设备的数据传输线路(类似于一条马路,把路边各住户连接起来,使住户可以相互交流)

总线就是一种运输方式,这里只是做一个简单介绍,后面会详细介绍 。

就像是一个大马路连接了路边所有住户,所有设备可以通过这条总线进行相互通信。

1.4 51单片机的UART模式

STC89C52有1个UART

STC89C52的UART有四种工作模式:

  •   模式0:同步移位寄存器
  •   模式18UART,波特率可变(常用)
  •   模式29UART,波特率固定
  •   模式39UART,波特率可变
  •  
    •         波特率又是一个我们经常听到的东西,那么它是什么呢?其实就是一个约定的速率,就是我们串口通信的速率(发送和接收各数据位的间隔时间)

      •         还有检验位是用于数据验证的。打开这个软件

      • 打开

      • 这里有几个校验位:就是用来检测数据的正确性的。

      • 这里主要介绍最常用的两个:奇校验和偶校验

      • 奇校验:

        •         给定一串二进制数据如1001 1100,设置该数据的1的个数为奇数个,如果原本的数据中1的个数就是奇数,那么设置校验位为0,否则设置为1,这里的1001 1100就可以把校验位设置为1,然后再把传输后的数据进行同样的操作,比较校验位是否相同,如果不相同则说传输数据出错了。

      • 偶校验:

        •         和奇校验类似,控制二进制数据中1的个数为偶数个,设置校验位。然后再在传输数据之后设置校验位。

          •         很显而易见的是,奇校验和偶校验是可以一定程度上检验数据的准确性的,但是还是有很多情况是无法检测错误的,比如1111 0011变成1100 0011,这样我们的奇校验和偶校验都无法检验出来。

停止位用于数据帧的间隔,即数据之间有间隔,我们使用停止位来占用这部分位。

8位数据和9位数据格式的区别就是9位数据可以用来设置一位为校验位。

2.串口配置

2.1寄存器简介

        我们这里配置最常见的串口模式——模式1

        这是串口模式配置图,这里有一个很重要的寄存器,就是SBUF寄存器,也叫串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器,读操作时,读出的是接收寄存器

        最左边的就是总线,两个不同意义上的SBUF寄存器使用一个总线连接,从输入端进入就会从输出端出。

        其中的这部分是使用定时器1实现的,主要作用就是控制波特率,也就是我们的数据收发速率。

       有了前面配置定时器0的经验,现在我们来配置这个串口的中断也是比较简单的,还是配置一下寄存器和使能还有中断优先级。所以我们来直接看寄存器表格:

        首先来介绍一下SCON寄存器:

        SCON 还是可位寻址,所以我们还是可以直接一位一位的配置这个寄存器。但是我们一般都是直接配置SCON的,如果想要一个一个配也是可以的。

        再来看看PCON寄存器:

        这个寄存器本来是用来控制电源的,但是这里多了两个控制模式的位SMOD和SMOD0,原因就是SCON寄存器的位满了,而中国PCON还有几个空闲的位,所以干脆就直接把这两个模式控制的就放在PCON里了。


        SCON寄存器配置

        我们一般使用的是方式1,所以这里我们要把SM0设为0,SM1设为1。并且,我们需要SM0为方式判断的位,我们还要把PCON中的第六位置为0。

        REN即允许/禁止串口接收数据,我们需要使用软件置位,我们需要使用接收数据就置REN为1,否则我们就置REN为0.

 

TI和RI我们在前面原理图的地方有见到过:

        TI和RI任意一个为1,就会导致跳转到中断函数,其中TI为发送中断请求标志位,RI为接收中断请求标志位。我们需要把这两个在初始时都标为0,然后一般由硬件置为1,当中断启用的时候,我们就必须把它们再次在软件中置为0.

 

        这三个我们是不需要管的,这三个是和方式2和方式3相关联,我们使用的是方式1,所以我们不配置,直接置为0都可以。

        总的来说,如果我们只需要串口发送数据,我们只要把SCON中的SM1置为1,其他的置为0就可以了,这样我们就可以直接配置SCON = 0x40;或者是分开来。如果我们需要串口接收数据,就还要加一个REN = 1,即SCON = 0x50.


        PCON配置

        上面讲到说我们的SCON中需要SM0和SM1共同配置我们的串口运作的方式,而不是用来检测错误,我们就需要把PCON中的第六位置为0.

        所以我们就要把SMOD0 = 0先设好。

        而PCON的第七位SMOD是用来设置方式123的波特率是否加倍:

        这一点我们在原理图上也可以看到: 

        我们让SMOD = 0,就说明我们的波特率需要除以2,也就是我们说的“不加倍”,然后SMOD=1就说明我们的波特率不用除以2,也就是要加倍。


         最后还有几个寄存器就简要介绍一下:

SADEN (Slave Address Enable):SADEN是一个特殊功能寄存器,用于启用或禁用I2C总线中从设备的地址匹配。当SADEN为1时,从设备的地址将参与地址匹配过程;当SADEN为0时,该从设备的地址将被忽略。

SADDR (Slave Address):SADDR是用于设置51单片机作为I2C从设备时的从设备地址。通过配置SADDR,可以将从设备的地址与主设备进行匹配,以便进行通信。SADDR的值可以是7位或10位,取决于所使用的I2C工作模式。

SADEN和SADDR都是用于配置51单片机作为I2C从设备时的地址匹配功能。SADEN用于启用或禁用地址匹配,而SADDR用于设置从设备的地址。这些功能使得51单片机能够与其他I2C设备进行通信。

然后就是老生常谈的IE使能位

        这里我们只要把EA和ES单独拿出来配置就好了,两个为0就禁止中断,两个为1就开放中断,EA主要是针对CPU的中断允许控制,ES则是针对串口的中断允许控制只要我们需要开放中断系统,我们就要把这两个都设置为1.

最后就是IP/IPH配置中断优先级,我们只有一个中断的时候就不用管,不用配置都可以,需要的时候按照下面配置一下就好了:

2.2代码配置串口

2.2.1  配置串口发送数据

        我们先根据上面的介绍,配置一个简单的单片机串口向电脑端发送数据的代码。

        首先,我们讲过,我们如果串口只要发送数据,我们就只要配置SCON为0x40,然后我们的PCON中波特率要加倍就把SMOD配置为1,这里我们就不加倍配置为0,然后SMOD0是控制SCON的SM0和SM1对应模式还是检验的前提,我们需要使用SM0和SM1把串口配置为模式1,所以我们的SMOD0必须为0,至于PCON里的其他配置暂时我们先不考虑,我们一会会通过软件生成

就有:        

SCON = 0x40;

        这里我们只要单片机向电脑发送数据,所以我们不需要使用中断,但是我们发送数据需要一定的波特率即发送的速率,这样的速率需要很精确的时钟计时,否则会发生数据传输出错的严重失误。

        我们在这里就使用定时器实现严格的时序控制,从而确保串口通信的稳定性和可靠性。并且我们需要更加精确的定时器,所以我们这里选择定时器1并且使用8位自动重载实现更加精确的计时。

        但是波特率的配置时比较复杂的,所以我们使用软件工具:

        像这样实现了我们的配置之后,我们就可以把代码复制出来使用了。一般来说,波特率设置越高误差越大,当然这个针对其他频率的51单片机而言的,我们的这个11.0592MHz是非常精确的一个频率,所以我们使用9600就可以了,其他频率的单片机比如12MHz的就需要使用4800在点击波特率加倍来使得这个误差减小,我们的11.0592就直接选9600就好了。

生成代码:

void UartInit(void)		//9600bps@11.0592MHz
{
	PCON &= 0x7F;		//波特率不倍速
	SCON = 0x50;		//8位数据,可变波特率
	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T
	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xFD;		//设定定时初值
	TH1 = 0xFD;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}

同样的,这里有的东西是需要更改的比如这个AUXR是更高级的单片机寄存器,我们就可以删掉,因为我们单片机没有这个东西,SCON可以配置为0x40,取决于自己的需求。

更改之后代码为:

void UartInit(void)		//9600bps@11.0592MHz
{
	PCON &= 0x7F;		//波特率不倍速
	SCON = 0x40;		//8位数据,可变波特率
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xFD;		//设定定时初值
	TH1 = 0xFD;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}

        这里我们设置ET1 = 0是因为我们实现单片机发送数据只需要定时器1作为一个时钟计算波特率,而不需要定时器1实现中断,所以就把它置为0禁止定时器1中断。


        配置好初始化函数,我们就再来配置一个发送数据的子函数,使用这个函数就可以在TI发送标志位被置为1的时候发送数据,并且在函数内把它重新置为0。发送数据要我们把数据发送到SBUF寄存器中。这样一个简单的发送数据的函数就写好了:

void SendByte(unsigned char buffer)
{
    SBUF = buffer;
    while(TI== 0);//标志位为0时停留在函数内部
    TI = 0;//在此处TI变为1,我们需要在函数中将其置为0
}

        注意,我们这里需要发送的数据应该是个8位的二进制,又因为我们在单片机中二级制无法直接表示,所以我们转化成十六进制再发送到SBUF中。

#include <REGX52.H>
void UartInit(void)	
{
	PCON &= 0x7F;	
	SCON = 0x50;	
	TMOD &= 0x0F;	
	TMOD |= 0x20;	
	TL1 = 0xFD;	
	TH1 = 0xFD;	
	ET1 = 0;		
	TR1 = 1;		
}

void SendByte(unsigned char buffer)
{
    SBUF = buffer;
    while(TI== 0);
    TI = 0;
}

void main()
{
	UartInit();
	SendByte(0x12);
	while(1)
	{
	}
}

        这里就是一个发送0x12的程序,我们这里没有把发送数据的子函数放在while循环中是因为我们把它放在while循环中就会导致一次发太多数据,不好观察,而且会出错,所以我们把它放在while循环前面,并且使用按下单片机上的复位按键来重置发送数据。

然后打开串口调试小助手,看一下发送的是不是对的。

STC-ISP在这里,要注意这些需要注意的东西,而且使用前串口是关闭的,需要我们手动打开串口,这个串口就是我们用来下载程序的CH340驱动的接口。

PZ-ISP就注意这里:

        大部分的都是一样的,只是两个软件不能同时使用,所以需要注意一下

注意:到这里看一下是不是发送的数据都是想要的值,如果按下复位键还是没有显示,检查一下是不是开启的是字符发送,需要改成HEX才可以看的到,或者是串口没有打开。这里如果显示的和自己的不太一样,检查一下波特率设置是否正确,如果还是不一样,建议重新生成代码,并且检查是不是选择的是定时器1的8位自动重载模式。

如果想要把发送的函数放在while循环中,可能会出现发送的数据不对的情况,这个可能是因为发送的速度太快,可以在函数后面加个Delay函数试试。

2.2.2配置电脑向单片机发送数据点亮LED

        还是按照上面的流程,我们直接在工具里生成代码:这里把多余的寄存器AUXR删除,其他的就不要变了,SCON保持0x50

        或者是直接把前面的代码复制下来,直接把SCON改成0x50也可以

void UartInit(void)		//9600bps@11.0592MHz
{
	PCON &= 0x7F;		//波特率不倍速
	SCON = 0x50;		//8位数据,可变波特率
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xFD;		//设定定时初值
	TH1 = 0xFD;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}

        然后这里我们还需要配置串口中断,注意:是串口的中断而不是定时器的中断,即我们的电脑向单片机发送数据之后,单片机接收到这个串口数据的那一时刻,就处理这个信息让这个LED点亮,使用串口的中断会更好实现一点。

        主要是这里几个寄存器,IE使能中,我们只要设置EA和ES为1允许中断即可。

        于是就写好了初始化函数:

void UartInit(void)	
{
	PCON &= 0x7F;	
	SCON = 0x50;	
	TMOD &= 0x0F;	
	TMOD |= 0x20;	
	TL1 = 0xFD;	
	TH1 = 0xFD;	
	ET1 = 0;		
	TR1 = 1;	
	EA = 1;
	ES = 1;
}

然后是中断函数:

        查询中断号,我们可以知道我们要使用的是4号中断。

开始配置函数:

void UART_Routine() interrupt 4
{
	if(RI == 1)
	{
		P2 = SBUF;
		RI = 0;
	}
}

然后我们使用电脑端发送数据就可以控制我们的单片机LED了。这里注意我们要控制这个RI的取值在为1的时候才算接收到数据,之后就要把它重新置为0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1576474.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java集合进阶】LinkedList和迭代器的源码分析泛型类、泛型方法、泛型接口

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …

项目架构MVC,DDD学习

写在前面 本文一起看下项目架构DDD&#xff0c;MVC相关的内容。 1&#xff1a;MVC 不管我们做什么项目&#xff0c;自己想想其实只是做了三件事&#xff0c;如下&#xff1a; 其实&#xff0c;这三件事完全在一个类中做完也可以可以正常把项目完成的&#xff0c;就像下面这…

MySQL-主从复制:概述、原理、同步数据一致性问题、搭建流程

主从复制 1. 主从复制概述 1.1 如何提升数据库并发能力 一般应用对数据库而言都是“读多写少”&#xff0c;也就说对数据库读取数据的压力比较大&#xff0c;有一个思路就是采用数据库集群的方案&#xff0c;做主从架构、进行读写分离&#xff0c;这样同样可以提升数据库的并…

mysql结构与sql执行流程

Mysql的大体结构 客户端&#xff1a;用于链接mysql的软件 连接池&#xff1a; sql接口&#xff1a; 查询解析器&#xff1a; MySQL连接层 连接层&#xff1a; 应用程序通过接口&#xff08;如odbc,jdbc&#xff09;来连接mysql&#xff0c;最先连接处理的是连接层。 连接层…

【Linux】达梦数据库安装部署(附详细图文)

目录 一、安装前的准备工作 1.检查操作系统配置 &#xff08;1&#xff09;获取系统位数 getconf LONG_BIT &#xff08;2&#xff09;查看操作系统release信息 cat /etc/system-release &#xff08;3&#xff09;查询系统名称 uname -a &#xff08;4&#xff09;查看操…

基于Spring Boot的网上书城系统(带文档)

主要功能 本次设计任务是要设计一个网上书城管理系统&#xff0c;通过这个系统能够满足网上书城的管理及用户的图书信息管理及购物功能。系统的主要功能包括&#xff1a;首页、个人中心、用户管理、图书类型管理、图书分类管理、图书信息管理、我的收藏管理、系统管理、订单管…

websocket实践

文章目录 背景WebSocket API使用场景优点 实例步骤 1: 设置 WebSocket 服务器步骤 2: 创建客户端 HTML 页面步骤 3: 测试 WebSocket 通信注意事项实际操作 参考资料 WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它使得浏览器和服务器只需建立一个连接&#xff0c;…

SWM341系列应用(MPU屏应用)

SWM341系列 MPU屏应用 1、MPU屏写入时序设置&#xff08;设置单位为周期&#xff09;&#xff0c;根据ST7789规格书规定的最小时序要求&#xff0c;建议MPU屏时序按照ST7789手册配置,建议配置的参数注释。例如WRRise_CSRise&#xff0c;时序图要求是最低10ns&#xff0c;根据计…

1.8.3 卷积神经网络近年来在结构设计上的主要发展和变迁——GoogleNet/inception-v1

1.8.3 卷积神经网络近年来在结构设计上的主要发展和变迁——GoogleNet/ inception-v1 前情回顾&#xff1a; 1.8.1 卷积神经网络近年来在结构设计上的主要发展和变迁——AlexNet 1.8.2 卷积神经网络近年来在结构设计上的主要发展和变迁——VGGNet GoogleNet问题 在VGGNet简单堆…

分类预测 | Matlab实现ABC-LSSVM人工蜂群算法优化最小二乘支持向量机数据分类预测

分类预测 | Matlab实现ABC-LSSVM人工蜂群算法优化最小二乘支持向量机数据分类预测 目录 分类预测 | Matlab实现ABC-LSSVM人工蜂群算法优化最小二乘支持向量机数据分类预测分类效果基本介绍程序设计参考资料 分类效果 基本介绍 1.Matlab实现ABC-LSSVM人工蜂群算法优化最小二乘支…

Redis中的Sentinel(六)

Sentinel 选举领头Sentinel. 当一个主服务器被判断为客观下线时&#xff0c;监视这个下线主服务器的各个Sentinel会进行协商&#xff0c;选举出一个领头Sentinel,并由领头 Sentinel对下线主服务器执行故障转移操作。以下是Redis选举领头Sentinel的规则和方法: 1.所有在线的S…

LabVIEW厂房漏水检测监控系统

LabVIEW厂房漏水检测监控系统 随着信息技术和智能制造的快速发展&#xff0c;对于精密仪器和重要物品存放场所的环境监控日益重要&#xff0c;特别是防止漏水带来的潜在风险。漏水不仅可能导致珍贵资料或仪器的损坏&#xff0c;还可能引发安全事故&#xff0c;给企业和研究机构…

在 C++ 中轻松实现字符串与字符数组的相互转换

在 C 中轻松实现字符串与字符数组的相互转换 引言一、将字符串转换为 char 数组1.1、C 中的 c_str()和 strcpy()函数1.2、使用 for 循环中的字符串到字符数组的转换 二、将 char 数组转换为字符串2.1、C 运算符 2.2、C 重载 运算符2.3、C 字符串内置构造函数 三、总结 引言 本…

阿里云服务器可以干嘛 阿里云服务器应用场景有哪些

阿里云服务器可以干嘛&#xff1f;能干啥你还不知道么&#xff01;简单来讲可用来搭建网站、个人博客、企业官网、论坛、电子商务、AI、LLM大语言模型、测试环境等&#xff0c;阿里云百科aliyunbaike.com整理阿里云服务器的用途&#xff1a; 阿里云服务器活动 aliyunbaike.com…

qt环境搭建-镜像源安装Qt Creator(5.15.2)以及配置环境变量

前言&#xff1a; 版本&#xff1a;5.15.2 镜像源&#xff1a;ustc与清华 纯小白&#xff0c;找了半天的镜像源安装qtcreator&#xff0c;搞了半天结果安装的是最新的&#xff0c;太新的对小白很不友好&#xff0c;bug比较多&#xff0c;支持的系统也不全&#xff0c;口碑不…

011_C标准库函数之<time.h>

头文件<time.h>中说明了一些用于处理日期和时间的类型和函数。其中的一部分函数用于处理当地时间&#xff0c;因为时区等原因&#xff0c;当地时间与日历时间可能不相同。clock_t和time_t是两个用于表示时间的算术类型&#xff0c;而struct tm则用于存放日历时间的各个成…

配置vscode用于STM32编译,Debug,github上传拉取

配置环境参考&#xff1a; Docs 用cubemx配置工程文件&#xff0c;用VScode打开工程文件。 编译的时候会有如下报错&#xff1a; vscode出现process_begin :CreateProcess failed 系统找不到指定文件 解决方案&#xff1a;在你的makefile中加上SHELLcmd.exe就可以了 参考…

mysql jdbc数据库速成总结

第一步导图jar包 我们下载一个jar 按照我的习惯是把这个jar包放在桌面上 方便后续操作 然后对这个jar包ctrl c复制 接着我们在idea里面创建一个目录 我们命名为lib 然后在这个lib安ctrl V进行粘贴 然后右键这个lib 找到添加为库 我的idea好像没有选择 只有添加为模块 点…

【LeetCode: 455. 分发饼干 + 贪心】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

2024/4/1—力扣—最小高度树

代码实现&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/ struct TreeNode* buildTree(int *nums, int l, int r) {if (l > r) {return NULL; // 递归出口}struct…