stm32串口自定义协议接收一串十六进制数据(将其中两个字节转化为十进制数据)+部分串口基础知识

一、基本储存单元

  • 位(bit):

    二进制数中的一个数位,可以是0或者1,是计算机中数据的最小单位。

  • 字节(Byte):

    计算机中数据的基本单位,每8位组成一个字节。各种信息在计算机中存储、处理至少需要一个字节。

    例如,一个ASCII码用一个字节表示,一个汉字用两个字节表示。

  • 字(Word):

    两个字节称为一个字。汉字的存储单位都是一个字。

1个字节(byte)=8位(bit)数据
1个汉字是一个字,即两个字节,16位数据

二、通信协议

通信方式-框图-参考链接

(一)并行通信和串行通信

  • 并行通信

    8位数据并列的传输,传一个8位的数据是需要8根线一起传输。

    例如:SDIO,FSMC(16位的)–发送数据都是所有数据位同时传输
    在这里插入图片描述

  • 串行通信

    8位数据一位一位的传输,只需要一根线即可进行传输。

    例如:USART,IIC,SPI三种都是串行方式-----发送数据时都是一位一位的进行发送数据
    在这里插入图片描述

  • 串行通信与并行通信特征

在这里插入图片描述

(二)三种工作方式

  • 全双工

    有两根数据线,一个用来接收数据,一个用来发送数据,互不干扰,可以同时发送和接收数据。

    例如:usart(可以半双工或全双工通信),SPI(可以半双工或者全双工通信)

  • 半双工

    有两根数据线,但是不可以同时发送数据,可以分时收发数据

  • 单工

    只有一根数据线,只可以单向通信(只可以往某一个方向进行)

    例如:IIC

000-串口通讯的单工、半双工和全双工的定义、区别及应用-

(三)收发数据同步或异步传输

  • 同步通信

    数据同步方式,两个设备的时钟信号是同一个(有时钟信号的是同步)。

    在传输数据时为了保证数据传输的准确性:
    (1)时钟在高电平时,数据有效
    (2)时钟信号在低电平时数据时无效的
    (3)对时钟的要求很高(如果时钟有尖峰或者杂波,则数据传输就不准确了)

    例如:SPI,IIC通信接口。
    在这里插入图片描述

  • 异步通信

    没有时钟信号:为了保证数据传输的准确性是通过加上一些辅助的标识符

    例如:UART(通用异步收发器),单总线。
    在这里插入图片描述

  • 同步通信和异步通信比较

    (1)在同步通信中,数据信号所传输的内容绝大部分就是有效数据。
    (2)在异步通信中,传输的数据会包含有帧的各种标识符。
    (3)所以同步通信的效率更好,但是对于时钟允许误差较小,异步通信对于时钟允许误差较大。

(四)通信速率

  • 比特率:每秒钟传输的二进制数单位:bit/s

    IIC,SPI(同步通信,一个时钟下传输一个数据,通过时钟来控制)

  • 波特率:每秒钟传输的码元个数(串口)

    一个二进制位表示一个码元(特殊情况下)

三、串口通信协议

物理层规定我们用嘴巴还是肢体进行交流
协议层就是规定我们用汉语还是英文来交流

(一)RS232

1、RS232和TTL就是在电平上的区别

TTL电平是直接从单片机(或者芯片)里面出来的:高电平用3.3V或者5v来表示,低电平用0表示
RS232中1用-15V表示,0用+15V表示,逻辑正好时相反的,低电平和高电平的差距非常大
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(二)USB转串口(TTL标准)

在这里插入图片描述

(三)串口到串口(TTL->TTL)

在这里插入图片描述

(四)串口数据包的基本组成

在这里插入图片描述

  • 奇校验

在这里插入图片描述

  • 偶校验

在这里插入图片描述

四、寄存器

(一)状态寄存器:USART_SR

在这里插入图片描述

  • TXE-发送数据寄存器空

    当TDR寄存器中的数据被硬件转移到移位寄存器的时候,TXE位被硬件置位。
    如果USART_CR1寄存器中的TXEIE为1,则产生中断。对USART_DR的写操作,将该位清零。

    0-数据还没有转移到移位寄存器
    1-数据已经转移到移位寄存器

  • TC-发送完成

    当包含有数据的一帧发送完成后,并且TXE=1时,由硬件将TC位置’1’。
    如果USART_CR1中的TCIE=1,则产生中断。由软件序列清除该位(先读USART_SR,然后写入USART_DR)。 TC位也可以通过写入’0’来清除,只有在多缓存通讯中才推荐这种清除程序。

    0-发送还没有完成
    1-发送已经完成

  • RXNE-读数据寄存器非空

    当RDR移位寄存器中的数据被转移到USART_DR寄存器中,该位(RXNE)被硬件置位。
    如果USART_CR1寄存器中的RXNEIE=1,则产生中断。对USART_DR的读操作可以将该位清零。 RXNE位也可以通过写入0来清除,只有在多缓存通讯中才推荐这种清除程序。

    0-数据没有收到
    1-收到数据可以读出

(二)数据寄存器USART_DR

在这里插入图片描述

(三)控制寄存器1(USART_CR1)

在这里插入图片描述

  • UE是使能串口 (1-模块使能)
  • TE是发送使能(0-禁止发送,1-使能发送)
  • RE是接收使能(0-禁止接收,1-使能接收)
  • PEIE是PE中断使能(0-禁止产生中断,1-当USART_SR的PE为1时,产生USART中断)
  • TCIE是发送完成中断使能(0-禁止产生中断,1-当USART_SR中的TC为’1’时,产生USART中断)
  • RXNEIE:接收缓冲区非空中断使能(0-禁止产生中断,1-当USART_SR中的ORE或者RXNE为’1’时,产生USART中断)

(四)串口接收和发送数据

  • 发送数据流程

在这里插入图片描述

此时UE=1,TE=1
上图对应的顺序依次是1->2->3,数据来源于CPU或者DMA,数据来了之后首先是放到发送寄存器(TDR)中,然后会再放到发送数据移位寄存器中,由于数据是8位的,会将数据一位一位的发送出去(使用TX引脚)。

  • 当数据由发送寄存器到发送数据移位寄存器时,TXE会被置1,即TXE=1,表示发送数据寄存器为空了,但是并不表示数据已经发送出去了。
  • 因为还要通过发送移位寄存器进行一位一位的发送数据,如果发送移位寄存器数据全部发送出去了,TC会被置1,即TC=1表示发送数据完毕。

**

  • 接收数据流程

**

在这里插入图片描述
此时UE=1,RX=1
数据从RX引脚进来来,数据是一位一位进行接收的。

  • 首先放到数据接收移位寄存器
  • 然后将数据传递到数据接收寄存器(RDR),此时RXNE会被置1,即RXNE=1,表示数据接收寄存器不是空的(收到数据可以读出)

五、STM32固件库函数

在这里插入图片描述
几个常用的固件库函数
在这里插入图片描述
在这里插入图片描述

  • 串口初始化函数

    void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);引脚、波特率、位数,校验、时钟等
    
  • 串口使能

     USART_Cmd(USART1, ENABLE); //使能串口—配置的是UE位
    
  • 中断使能

     void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT,FunctionalState NewState)
    
  • 发送数据

    STM32 库函数操作 USART_DR 寄存器发送数据的函数是:通过该函数向串口寄存器 USART_DR 写入一个数据。

    void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
    
  • 接收数据

    STM32 库函数操作 USART_DR 寄存器读取串口接收到的数据的函数是:通过该函数可以读取串口接收到的数据。

     uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
    
  • 获取标志位

    该函数只判断标志位。在没有使能相应的中断函数时,通常使用该函数来判断标志位是否置1

     Flagstatus USART_GetFlagStatus(USARTx,USART_FLAG)
    
  • 中断状态位获取函数

    不仅会判断标志位是否置1,同时还会判断是否使能了相应的中断。所以在串口中断函数中,如果要获取中断标志位,通常使用该函数。

     ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t  USART_IT)
    
  • 清楚中断标志位

     Void  USART_Flag_Clear(USARTx,USART_FLAG)
    

几个标志位函数的区分说明–链接

六、USART应用

串口初始化函数

//初始化IO 串口1 
//bound:波特率
void uart_init(u32 bound){
    //GPIO端口设置
		GPIO_InitTypeDef GPIO_InitStructure;//GPIO结构体指针
		USART_InitTypeDef USART_InitStructure;//串口结构体指针
		NVIC_InitTypeDef NVIC_InitStructure;//中断分组结构体指针
		//1、使能串口时钟,串口引脚时钟 
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟
		
	//2、复位串口	
		USART_DeInit(USART1);  //复位串口1
	
	//3、发送接收引脚的设置
	 //USART1_TX   PA.9(由图 可知设置为推挽复用输出)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA9
   
    //USART1_RX	  PA.10(有图可知浮空输入)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);  //初始化PA10


   //4、USART 初始化设置

		USART_InitStructure.USART_BaudRate = bound;//一般设置为9600;
		USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
		USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
		USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
		USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
		USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式

    USART_Init(USART1, &USART_InitStructure); //初始化串口
		
#if EN_USART1_RX		  //如果使能了接收  
   //5、Usart1 NVIC 配置
		NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
		NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
		NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
		NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
		NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器
   
	  //6、开启接收数据中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
		
#endif
		//7、使能串口
    USART_Cmd(USART1, ENABLE);                    //使能串口 

}

(一)发送数据

1、函数1-串口发送一个字节数据

void Usart_SendByte(USART_TypeDef* pUSARTx,uint8_t data)
{
	//调用固件库函数
	USART_SendData(pUSARTx,data);//往串口中写入数据
	
	//发送完数据是检测TXE这个位是否置1,发送数据寄存器空了,表明已经把数据传递到数据移位寄存器了
	//检测TXE这个位也需要一个固件库函数
	while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE)==RESET);
	//如果这个位一直为0的话就一直等待,只有当被设为SET后才会跳出这个循环(表示一个字节发送出去了)
}

在main主函数中编写语句:

//在主函数里面发送一个数据试试
Usart_SendByte(USART1,100);//往串口1中写入数据100

串口调试助手并没有显示100,而是显示的一个字母d
串口调试助手不管接收到的是什么数据,都会转化为字符
只有发送十六进制数据,串口助手使用十六进制形式接收数据时才不是字符

Usart_SendByte(USART1,'A');//往串口1里面写入一个字符A
//串口接收到字符A

串口助手无论是收发都是以字符的形式进行传输的

假如说串口助手发送一个数字1,stm32串口如果能够接收的话,在进行数据解析过程中需要按照字符 ’1‘ 来进行解析(把1当成是字符,而不是十进制1 )

2、函数2-发送两个字节数据

有时候传感器数据可能是16位的,怎么发送?发送两个字节?

发送两个字节的数据就是十六位的。

//发送两个字节数据函数
void Usart_SendHalfWord(USART_TypeDef* pUSARTx,uint16_t data)
{
	//发送十六位数据要分为两次来发送,先定义两个变量
	uint8_t temp_h,temp_l;//定义8位的变量(分别存储高8位和低8位)

	//首先取出高8位
	temp_h=(data&0xff00)>>8;//低八位先与0相&,低8位变为0再右移8位(0xff00共16位二进制)
	//再取出低8位
	temp_l=data&0xff;//取出低8位数据
	//16位的数据这样子就放到了两个变量里面(共16位)
	
	//调用固件库函数
	USART_SendData(pUSARTx,temp_h);//先发送高8位
	while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE)==RESET);//等待数据发送完毕

	USART_SendData(pUSARTx,temp_l);//再发送低8位
	while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE)==RESET);//等待数据发送完毕

}

在主函数中发送十六进制数据:

Usart_SendHalfWord(USART1,0XFF56);//发送16位数据

串口助手显示的是字符,要想接收到的数据和发送的一样,需要把串口助手选择为16进制接收
串口助手接收到ff 56。虽然是16位的数据但是显示的时候还是一个字节一个字节的显示,十六进制ff是一个字节 56是一个字节
在这里插入图片描述

3、函数3-发送一个数组数据

//发送一个数组数据
void Usart_SendArray(USART_TypeDef* pUSARTx,uint8_t *array,uint8_t num)
{
	//每次想要发送多少数据,通过形参num传进来,num定义的是8位的,那么函数最多发送255个
	int i;
	for(i=0;i<num;i++)
	{
		//调用发送一个字节函数发送数据(下面两种写法都可以)
		//Usart_SendByte(USART1,*array++);
		Usart_SendByte(USART1,array[i]);//每次只能发送8位数据
	}
	while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);//等待发送完毕
}

判断发送一个字节的数据标志位:USART_FLAG_TXE
判断发送一连串字节的数据标志位:USART_FLAG_TC

在主函数中定义一个数组

uint8_t a[12]={1,2,3,4,5,6,7,8,9,10,98,100};

将数据内容发送出去

Usart_SendArray(USART1,a,12);

串口助手:十六进制形式接收数据
在这里插入图片描述

串口助手:非十六进制数据形式接收数据:1-10的ASCII是无法显示的
在这里插入图片描述

4、函数4-发送字符串函数

//发送字符串
	void Usart_SendStr(USART_TypeDef* pUSARTx,uint8_t *str)//指定串口,要发送的字符串内容
	{
		uint8_t i=0;
		//使用do-while循环,do的时候已经开始发送了
		do{
			//需要调用发送一个字节函数
			Usart_SendByte(USART1,*(str+i));//发送一次之后指针地址后移一个
			i++;
		}while(*(str+i)!='\0');//最后结尾不等于'\0'为真,继续发送
		//如果='\0'表示发送完毕
		while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);//等待发送完毕
	}

在主函数中调用函数发送一个字符串

Usart_SendStr(USART1,"欢迎使用stm32\n");//此时发送的是字符,串口助手要取消十六进制接收

5、函数5-使用printf函数进行打印数据

有时候想直接用printf函数直接发送,肯定是不可以的
printf函数底层会有一个fputc,如果想要使用,需要重新定义

int fputc(int ch FILE *f)
{
	//发送一个字节数据到串口
	USART_SendData(USART1,(uint8_t)ch);
	//等待发送完毕
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
	//检测发送数据寄存器是否为空USART_FLAG_TXE

	return (ch);
}

在主函数中编写代码

printf("串口测试实验\n");
//发送一个字符也可以直接使用putchar('a')

putchar('a');//串口助手会接收到字母a

6、函数6-使用getchar函数

getchar()等价于scanf()函数
如果使用getchar函数也需要重新定义
重定向c库函数scanf到串口,重写后可以使用scanf和getchar函数

重定向c库函数scanf到串口,重写后可以使用scanf和getchar函数
int fgetc(FILE *f)
{
	//等待串口输入数据
	/* 有了这个等待就不需要在中断中进行了 */
	while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==RESET);
	return (int)USART_ReceiveData(USART1);
}

如果在主函数中使用getchar()需要把下面的中断设置代码注释掉(下图),否则会冲突
因为不需要在中断中进行

在这里插入图片描述
在主函数的while(1)循环中加入接收数据和发送数据的代码

ch=getchar();
printf("ch=%c\n",ch);//打印接收到的字符

用串口助手发送什么字符,串口助手就会接收到单片机返回什么字符
在这里插入图片描述

(二)接收数据

当接收到数据时产生中断

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断

有中断就要设置中断优先级

NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器

编写中断服务函数

1、串口助手发送并返回数据中断函数

串口助手发送什么数据给单片机,单片机自动将接收到的数据返回给串口助手

//中断服务函数
void USART1_IRQHandler() 
{
	u8 ucTemp;
	if(USART_GetFlagStatus(USART1,USART_IT_RXNE)!=RESET)
	{
		ucTemp=USART_ReceiveData(USART1);
		USART_SendData(USART1, ucTemp);
	}
}

当外部设备或者串口调试助手给单片机发送数据时,单片机检测到数据接收寄存器非空,表示数据来了此时产生中断,进去中断服务函数调用固件库函数这个标志位是否真正置1,以免产生误中断,如果真的产生1时,调用USART_ReceiveData(USART1)函数接收数据,把数据放ucTemp变量中
再调用USART_SendData(USART1, ucTemp)把数据发送回串口助手

2、串口发送数据控制led亮灭

通过串口接收到的数据进行控制led灯,这样子就不需要中断来接收了,通过查询方法即可,这时候要把中断部分给注释了,中断服务函数注释掉就可以了

在主函数中加入以下代码:

   u8 ch;//存放电脑接收到的数据
	while(1)
	{
		ch=getchar();//读取串口数据
		printf("ch=%c\n",ch);//打印接收到的字符
		switch(ch)//进行匹配
		{
			case '1':
				LED0=0;break;//打开LED0
			case '2':
				LED1=0;break;//打开LED1
		}

	}

如果出现错误:参考此链接
解决方案-链接
使用串口助手分别发送1和2就可以控制led灯的亮灭了
在这里插入图片描述

3、STM32自定义协议接收十六进制数据(使用三合一气体传感器求出CO2浓度)

前面已经说到,如果不勾选十六进制数据接收和发送的话,其他情况都是以字符的形式发送和接收的,所以此处在发送和接收数据时都要将hex选项进行勾选

三合一气体传感器串口数据流格式如下:
在这里插入图片描述

由于前两个字节数据是固定的模块地址,因为可以用来当做判断标准,定义一个接收数据的协议:只有第一个字节和第二个字节都符合条件时,才将数据存储到数组中

模块数据流:2C E4 04 00 00 AD 03 38 FC

数据流共9个字节,所以先定义一个存储9个字节的八位数组

u8 table_data[9];//这是提前定义一个数组存放接收到的数据
u8 table_cp[9];//这是额外定义一个数组,将接收到的数据复制到这里面
u16 count=0;//接收数据计数

编写中断服务函数

//使用自定义协议接收十六进制数据

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
		u8 Res,i;
		if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断
		{
			Res =USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的数据
			
			if(count==0)//如果是接收的第一个数据
			{
				table_data[count]=Res;//将第一个数据存到数据中第一元素
				if(table_data[0]==0x2c)//判断接收的第一个数据是不是十六进制0X2C
				  count++;//如果第一个数据是0X2C则表示正确计数+1
			}
			else if(count==1)//第一个数据接收正确的情况下,判断第二个数据
			{
				if(Res==0xe4)//如果刚接收的数据是0xE4则表示数据正确
				{
					table_data[count]=Res;//将数据储存到数组第二个元素位置
					count++;//接收数据计数+1
				}
				else//如果第二个字符不是0XE4则计数清零,重新接收
					count=0;
			}
			else if(count==2&&Res==0)//如果前两个数据正确,接收的第三个数据是0,则清零计数,重新接收数据
			{
				count=0;
			}
			else if(count>1&&count<9)//这是可以接收数据的范围,只要count在数据可接收数据范围内即可进行存入数据
			{
				table_data[count]=Res;
				count++;
			}
			else if(count>=9)//如果接收数据超过数组大小,则清零重新接收
			{
				count=0;
			}		
   } 
	 
		memset(table_cp, 0, sizeof(table_data));//在使用数组table_cp时清空
		for(i=0;i<9;i++)//把接收到的数据复制到table_cp数组中
		{
			 table_cp[i]= table_data[i];
	}
} 

上面实现的是通过自定义协议接收并存储数据

在接收到十六进制数据之后,想要提取其中的两个字节并将其转化为十进制数据,首先需要编写一个十六进制转换函数:(输入十六进制数据返回十进制数据

int hextoDec(int hex)
{
	 int sum=0,mul=1;
	 int i,r;
	 int count=0;
	 do{
	  r=hex%16;
	  for(i=0;i<count;i++)
	   mul*=16;
	  mul*=r;
	  sum+=mul;
	  mul=1;
	  count++; 
	 }while(hex/=16);
	 return sum;
}

主函数的while内容如下:

while(1)
	{
	  if(table_cp[0]==0x2c)//如果数组第一个十六进制数据是0X2C则进行
		{
			//用十进制数据打印一下接收到的数据
			//原始数据(十六进制数据)是2C E4 04 00 00 AD 01 23 FC
			//前两位是固定的,第7个和第8个十六进制数据分别是CO2的高八位和低八位
			for(i=0;i<9;i++)
			{
					printf(" %d \n",table_cp[i]);
				
			}
			
			printf("\r\n");//加一个回车换行
			//把对应的十六进制数据转化为十进制数据
			num_H=hextoDec(table_cp[6]);//高8位
			num_L=hextoDec(table_cp[7]);//低8位
			printf("hh=%d LL=%d \n",num_H,num_L);
			num=num_H*256+num_L;//使用CO2浓度计算公式计算出数值
			printf("CO2=%d\n",num);
				
			}
	}

在这里插入图片描述
验证如下:
如下图所示:使用串口助手发送十六进制数据:
2C E4 04 00 00 AD 03 38 FC

其中第7个字节是CO2的高8位,第8个字节是CO2浓度的低8位,然后再按照计算公式进行计算即可求出二氧化碳浓度
在这里插入图片描述

完整代码下载链接

版权声明:本文为CSDN博主「永相随1」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37281984/article/details/122279342

永相随1

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

STM32引脚模式:推挽、开漏、上拉、下拉、浮空

一、简介 ​ GPIO的配置种类有8种之多:模拟输入、浮空输入、下拉输入、上拉输入、 开漏输出、推挽输出、 复用开漏输出、 复用推挽输出,每次使用引脚时都需要进行配置,所以我以自己的理解&#xff0c