【STM32F429开发板用户手册】第23章 STM32F429的USART串口基础知识和HAL库API

最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255

第23章       STM32F429的USART串口基础知识和HAL库API

本章节为大家讲解USART(Universal synchronous asynchronous receiver transmitter,通用同步异步收发器)的基础知识和对应的HAL库API。

目录

第23章       STM32F429的USART串口基础知识和HAL库API

23.1 初学者重要提示

23.2 串口基础知识

23.2.1 串口的硬件框图

23.2.2 串口的基本功能

23.2.3 不同串口支持的特性异同

23.2.4 串口的数据帧格式

23.2.5 串口发送时序图

23.2.6 同步串口和异步串口的区别

23.2.7 单工,半双工和全双工通讯

23.3 串口的HAL库用法

23.3.1 串口寄存器结构体USART_TypeDef

23.3.2 串口句柄结构体UART_HandleTypeDef

23.3.3 串口的底层配置(GPIO、时钟、中断等)

23.3.4 串口的状态标志清除问题

23.3.5 串口初始化流程总结

23.4 源文件stm32f4xx_hal_uart.c

23.4.1 函数HAL_UART_Init

23.4.2 函数HAL_UART_Transmit

23.4.3 函数HAL_UART_Receive

23.4.4 函数HAL_UART_Transmit_IT

23.4.5 函数HAL_UART_Receive_IT

23.4.6 函数HAL_UART_Transmit_DMA

23.4.7 函数HAL_UART_Receive_DMA

23.5 总结


 

23.1 初学者重要提示

  1.   学习串口外设推荐从硬件框图开始了解基本的功能特性,然后逐步深入了解各种特性,这种方式方便记忆和以后查阅。而串口的通信学习,推荐看时序图。
  2.   部分中断标志是可以通过操作发送数据寄存器TDR或者接收数据寄存器RDR实现清除,这点要特别注意,详情看本章23.3.4小节。
  3.   初次使用USART,还是有不少注意事项的,详情看本章23.3.3小节和23.4.1小节。

23.2 串口基础知识

USART的全称是Universal synchronous asynchronous receiver transmitter,中文意思是通用同步异步收发器。我们经常使用串口是异步串口,简称UART。

23.2.1 串口的硬件框图

认识一个外设,最好的方式就是看它的框图,方便我们快速的了解串口的基本功能,然后再看手册了解细节。

通过这个框图,我们可以得到如下信息:

  •   TX和RX接口

分别用于数据的发送和接收。

  •   SW_RX接口

在智能卡模式下,此接口用于接收数据。

  •   IRAD_OUT和IRAD_IN接口

用于IRAD模式的数据发送和接收。

  •   RST和CTS接口

用于硬件流控制。

  •   USART_BRR

波特率生成单元。

  •   发送过程经过的寄存器

依次是USART_TDR -> TxFIFO ->Tx Shift Reg偏移寄存器 –> TX或者RX引脚。

  •   接收经过的寄存器

依次是TX或者RX引脚-> Rx Shift Reg偏移寄存器->RxFIFO –>USART_RDR。

23.2.2 串口的基本功能

STM32的串口功能很强大,支持太多的模式。我们只需关心我们最常用的特性即可。我们的串口驱动使用的串口中断+FIFO结构,没有使用DMA。因此我们只讨论和串口中断、串口常规参数有关的知识。

STM32串口的优越特性:(只列了举常用的)

  •   任意波特率。硬件采用分数波特率发生器系统,可以设置任意的波特率,最高达11.25Mbits/s。这一点很重要。比如ES8266串口WIFI芯片,上电时有个奇怪的波特率74880bps,当然STM32是可以支持的。
  •   可编程数据字长度,支持8bit和9bit。
  •   可配置的停止位。支持1或2个停止位。
  •   发送器和接收器可以单独使能。比如GPS应用只需要串口接收,那么发送的GPIO就可以节省出来用作其他功能。
  •   检测标志和中断:

a. 接收缓冲器满,可产生中断。串口中断服务程序据此判断是否接收到数据。

b. 发送缓冲器空,可产生中断。串口中断服务程序据此启动发送下一个数据。

c. 传输结束标志,可产生中断。用于RS485通信,等最后一个字节发送完毕后,需要控制RS485收发器芯片切换为接收模式。

 

其它中断不常用,包括:CTS改变、LIN断开符检测、检测到总线为空闲(在DMA不定长接收方式会用到)、溢出错误、帧错误、噪音错误、校验错误。

23.2.3 不同串口支持的特性异同

通过下面的表格,可以对串口1-8支持的功能有个全面的认识:

23.2.4 串口的数据帧格式

串口支持的帧格式如下(M和PCE都是USART_CR1寄存器的位,其中M位用于控制帧长度,PCE用于使能奇偶校验位):

 

这里特别注意奇偶校验位,用户在配置的时候可以选择奇校验和偶校验,校验位是占据的最高位。比如选择M=00,PCE=1,即7bit的数据位。

  •   串口发送数据:

如果发送的7bit数据是111 0011,这个里面有奇数个1,那么选择偶校验的情况下,校验位 = 1,凑够偶数个1,而选择奇校验的情况下,校验位 = 0,因为已经是奇数个1。校验位不需要用户去计算,是硬件自动生成的。

  •   串口接收数据:

根据用户设置的奇校验或者偶校验类型,串口硬件会对接收到的数据做校验,如果失败,USART_ISR寄存器的PE位会被置1。如果使能了对应的中断PEIE,那么失败的时候还会产生中断。

 

了解到帧格式后,再来看一下实际数据发送时,数据位的先后顺序:

 

23.2.5 串口发送时序图

这个时序图非常具有代表性,可以帮助大家很好的理解TC发送完成中断和TXE空中断。

 

23.2.6 同步串口和异步串口的区别

异步通信是按字符传输的。每传输一个字符就用起始位来进行收、发双方的同步,不会因收发双方的时钟频率的小的偏差导致错误。这种传输方式利用每一帧的起、止信号来建立发送与接收之间的同步。

异步的特点是:每帧内部各位均采用固定的时间间隔,而帧与帧之间的间隔是随机的。接收机完全靠每一帧的起始位和停止位来识别字符是正在进行传输还是传输结束。

同步通信的发送和接收双方要保持完全的同步,因此要求接收和发送设备必须使用同一时钟。优点是可以实现高速度、大容量的数据传送;缺点是要求发生时钟和接收时钟保持严格同步,同时硬件复杂。

可以这样说,不管是异步通信还是同步通信都需要进行同步,只是异步通信通过传送字符内的起始位来进行同步,而同步通信采用共用外部时钟来进行同步。所以,可以说前者是自同步,后者是外同步。

23.2.7 单工,半双工和全双工通讯

单工:在一个单工的串行通讯系统中,一般至少有两根线(信号线和地线),数据传送只有一个方向,例如可以使用单工数据传送将数据从一个简单的数据监测系统传送到PC上。

半双工:在半双工串行通信系统中,一般同样要求至少有两根线。这里的数据传送是双向的。然而,同一个时刻只能为一个方向。在上面的数据监测的例子中做了一些变化,可以使用半双工通讯机制发送信息到嵌入式模块(来设置参数,比如采样率)。此外,在其他时候,可以使用这个种连接将嵌入式装置上的数据下载到PC中。

全双工:在一个全双工的串行通信系统中,一般要求至少有三根线(信号线A,信号线B和地线)。信号线A将传输一个方向上的数据,同时信号线B传送另一个方向上的数据。

23.3 串口的HAL库用法

串口的HAL库用法其实就是几个结构体变量成员的配置和使用,然后配置GPIO、时钟,并根据需要配置NVIC、中断和DMA。下面我们逐一展开为大家做个说明。

23.3.1 串口寄存器结构体USART_TypeDef

USART相关的寄存器是通过HAL库中的结构体USART_TypeDef定义的,在stm32h743xx.h中可以找到这个类型定义:

typedef struct
{
  __IO uint32_t SR;         /*!< USART Status register,                   Address offset: 0x00 */
  __IO uint32_t DR;         /*!< USART Data register,                     Address offset: 0x04 */
  __IO uint32_t BRR;        /*!< USART Baud rate register,                Address offset: 0x08 */
  __IO uint32_t CR1;        /*!< USART Control register 1,                Address offset: 0x0C */
  __IO uint32_t CR2;        /*!< USART Control register 2,                Address offset: 0x10 */
  __IO uint32_t CR3;        /*!< USART Control register 3,                Address offset: 0x14 */
  __IO uint32_t GTPR;       /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;

这个结构体的成员名称和排列次序和CPU的USART寄存器是一 一对应的。

__IO表示volatile, 这是标准C语言中的一个修饰字,表示这个变量是非易失性的,编译器不要将其优化掉。core_m4.h文件定义了这个宏:

#define     __O     volatile             /*!< Defines 'write only' permissions */
#define     __IO    volatile             /*!< Defines 'read / write' permissions */

下面我们看下USART1、USART2 ... UART8的定义,在stm32f429.h文件

#define PERIPH_BASE           0x40000000UL 
#define APB1PERIPH_BASE       PERIPH_BASE
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000UL)

#define USART1_BASE           (APB2PERIPH_BASE + 0x1000UL)
#define USART6_BASE           (APB2PERIPH_BASE + 0x1400UL)
#define USART2_BASE           (APB1PERIPH_BASE + 0x4400UL)
#define USART3_BASE           (APB1PERIPH_BASE + 0x4800UL)
#define UART4_BASE            (APB1PERIPH_BASE + 0x4C00UL)
#define UART5_BASE            (APB1PERIPH_BASE + 0x5000UL)
#define UART7_BASE            (APB1PERIPH_BASE + 0x7800UL)
#define UART8_BASE            (APB1PERIPH_BASE + 0x7C00UL)

#define USART1              ((USART_TypeDef *) USART1_BASE) <----- 展开这个宏,(USART_TypeDef *) 0x40001010
#define USART2              ((USART_TypeDef *) USART2_BASE)
#define USART3              ((USART_TypeDef *) USART3_BASE)
#define UART4               ((USART_TypeDef *) UART4_BASE)
#define UART5               ((USART_TypeDef *) UART5_BASE)
#define USART6              ((USART_TypeDef *) USART6_BASE)
#define UART7               ((USART_TypeDef *) UART7_BASE)
#define UART8               ((USART_TypeDef *) UART8_BASE)

我们访问USART1的CR1寄存器可以采用这种形式:USART1->CR1 = 0。

23.3.2 串口句柄结构体UART_HandleTypeDef

HAL库在USART_TypeDef的基础上封装了一个结构体UART_HandleTypeDef,定义如下:

typedef struct __UART_HandleTypeDef
{
  USART_TypeDef                 *Instance;        /*!< UART registers base address        */
  UART_InitTypeDef              Init;             /*!< UART communication parameters      */
  uint8_t                       *pTxBuffPtr;      /*!< Pointer to UART Tx transfer Buffer */
  uint16_t                      TxXferSize;       /*!< UART Tx Transfer size              */
  __IO uint16_t                 TxXferCount;      /*!< UART Tx Transfer Counter           */
  uint8_t                       *pRxBuffPtr;      /*!< Pointer to UART Rx transfer Buffer */
  uint16_t                      RxXferSize;       /*!< UART Rx Transfer size              */
  __IO uint16_t                 RxXferCount;      /*!< UART Rx Transfer Counter           */
  DMA_HandleTypeDef             *hdmatx;          /*!< UART Tx DMA Handle parameters      */
  DMA_HandleTypeDef             *hdmarx;          /*!< UART Rx DMA Handle parameters      */
  HAL_LockTypeDef               Lock;             /*!< Locking object                     */
  __IO HAL_UART_StateTypeDef    gState;           /*!< UART state information related to global Handle
 management
                                                       and also related to Tx operations.
                                                       This parameter can be a value of @ref 
HAL_UART_StateTypeDef */
  __IO HAL_UART_StateTypeDef    RxState;          /*!< UART state information related to Rx operations.
                                                       This parameter can be a value of @ref
 HAL_UART_StateTypeDef */
  __IO uint32_t                 ErrorCode;        /*!< UART Error code                    */

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
  void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);       
  void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart);           
  void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);       
  void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart);            
  void (* ErrorCallback)(struct __UART_HandleTypeDef *huart);             
  void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart);         
  void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart);
  void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart);
  void (* WakeupCallback)(struct __UART_HandleTypeDef *huart);            
  void (* MspInitCallback)(struct __UART_HandleTypeDef *huart);          
  void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart);        
#endif 
} UART_HandleTypeDef;

这里重点介绍前两个参数,其它参数主要是HAL库内部使用的。

  •   USART_TypeDef  *Instance

这个参数是寄存器的例化,方便操作寄存器,比如使能串口的发送空中断。

SET_BIT(huart->Instance->CR1,  USART_CR1_TXEIE)。

  •   UART_InitTypeDef  Init

这个参数是用户接触最多的,用于配置串口的基本参数,像波特率、奇偶校验、停止位等。UART_InitTypeDef结构体的定义如下:

typedef struct
{
  uint32_t BaudRate;            /* 波特率 */
  uint32_t WordLength;          /* 数据位长度 */
  uint32_t StopBits;            /* 停止位 */
  uint32_t Parity;              /* 奇偶校验位 */
  uint32_t Mode;                /* 发送模式和接收模式使能 */  
  uint32_t HwFlowCtl;           /* 硬件流控制 */ 
  uint32_t OverSampling;        /* 过采样,可以选择8倍和16倍过采样 */
} UART_InitTypeDef;

配置串口参数,其实就是配置结构体UART_HandleTypeDef的成员。比如下面配置为波特率115200,8个数据位,无奇偶校验,1个停止位。

UART_HandleTypeDef UartHandle;

/* USART3工作在UART模式 */
/* 配置如下:
  - 数据位 = 8 Bits
  - 停止位 = 1 bit
  - 奇偶校验位 = 无
  - 波特率 = 115200bsp
  - 硬件流控制 (RTS 和 CTS 信号) */
UartHandle.Instance        = USART3;

UartHandle.Init.BaudRate     = 115200;
UartHandle.Init.WordLength   = UART_WORDLENGTH_8B;
UartHandle.Init.StopBits     = UART_STOPBITS_1;
UartHandle.Init.Parity       = UART_PARITY_NONE;
UartHandle.Init.HwFlowCtl    = UART_HWCONTROL_NONE;
UartHandle.Init.Mode         = UART_MODE_TX_RX;
UartHandle.Init.OverSampling = UART_OVERSAMPLING_16;

if(HAL_UART_Init(&UartHandle) != HAL_OK)
{
    Error_Handler();
}
  •   条件编译USE_HAL_UART_REGISTER_CALLBACKS

用于串口回调函数的设置。

如果要用到这种回调函数定义方式,可以在stm32f4xx_hal_conf.h文件里面使能。

23.3.3 串口的底层配置(GPIO、时钟、中断等)

串口外设的基本参数配置完毕后还不能使用,还需要配置GPIO、时钟、中断等参数,比如下面配置串口1,使用引脚PA9和PA10。

/* 串口1的GPIO  PA9, PA10 */
#define USART1_CLK_ENABLE()              __HAL_RCC_USART1_CLK_ENABLE()
#define USART1_TX_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOA_CLK_ENABLE()
#define USART1_TX_GPIO_PORT              GPIOA
#define USART1_TX_PIN                    GPIO_PIN_9
#define USART1_TX_AF                     GPIO_AF7_USART1

/*
*********************************************************************************************************
*    函 数 名: InitHardUart
*    功能说明: 配置串口的硬件参数和底层
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
static void InitHardUart(void)
{
    GPIO_InitTypeDef  GPIO_InitStruct;
    RCC_PeriphCLKInitTypeDef RCC_PeriphClkInit;

#if UART1_FIFO_EN == 1        /* 串口1 */
    /* 使能 GPIO TX/RX 时钟 */
    USART1_TX_GPIO_CLK_ENABLE();
    USART1_RX_GPIO_CLK_ENABLE();
    
    /* 使能 USARTx 时钟 */
    USART1_CLK_ENABLE();    

    /* 配置TX引脚 */
    GPIO_InitStruct.Pin       = USART1_TX_PIN;
    GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull      = GPIO_PULLUP;
    GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = USART1_TX_AF;
    HAL_GPIO_Init(USART1_TX_GPIO_PORT, &GPIO_InitStruct);    
    
    /* 配置RX引脚 */
    GPIO_InitStruct.Pin = USART1_RX_PIN;
    GPIO_InitStruct.Alternate = USART1_RX_AF;
    HAL_GPIO_Init(USART1_RX_GPIO_PORT, &GPIO_InitStruct);

    /* 配置NVIC the NVIC for UART */   
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 1);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  
    /* 配置波特率、奇偶校验 */
    bsp_SetUartParam(USART1,  UART1_BAUD, UART_PARITY_NONE, UART_MODE_TX_RX);

    CLEAR_BIT(USART1->SR, USART_SR_TC);      /* 清除TC发送完成标志 */
     CLEAR_BIT(USART1->SR, USART_SR_RXNE);   /* 清除RXNE接收标志 */
    // USART_CR1_PEIE | USART_CR1_RXNEIE
    SET_BIT(USART1->CR1, USART_CR1_RXNEIE); /* 使能PE. RX接受中断 */
#endif
}

总结下来就是以下几点:

  •   配置GPIO引脚时钟。
  •   配置USART时钟。
  •   配置USART的发送和接收引脚。
  •   通过NVIC配置中断。
  •   配置波特率,奇偶校验等,在上一小节有讲。
  •   清除TC和RXNE标志,使能接收中断。

 

关于这个底层配置有以下几点要着重说明下:

  •   串口发送和接收引脚的复用模式选择已经被HAL库定义好,放在了stm32f4xx_hal_gpio_ex.h文件里面。比如串口1有两个复用
#define GPIO_AF7_USART1    ((uint8_t)0x07)  /* USART1 Alternate Function mapping */

为什么使用的AF7,这个是在数据手册里定义好的:

那么使用GPIO_AF7_USART1即可。

  •   根据情况要清除TC发送完成标志和RXNE接收数据标志,因为这两个标志位在使能了串口后就已经置位,所以当用户使用了TC或者RX中断后,就会进入一次中断服务程序,这点要特别注意。
  •   HAL库有个自己的底层初始化回调函数HAL_UART_MspInit,是弱定义的,用户可以在其它的C文件里面实现,并将相对的底层初始化在里面实现。当用户调用HAL_UART_Init后,会在此函数里面调用HAL_UART_MspInit,对应的底层复位函数HAL_UART_MspDeInit是在函数HAL_UART_DeInit里面被调用的。当然,用户也可以自己初始化,不限制必须在两个函数里面实现。
  •   上面举的例子里面没有用到DMA,如果用到了DMA,也是要初始化的。

23.3.4 串口的状态标志清除问题

注,经常会有网友咨询为什么串口中断服务程序里面没有做清除标志。

下面我们介绍__HAL_USART_GET_FLAG函数。这个函数用来检查USART标志位是否被设置。

/** @brief  Check whether the specified USART flag is set or not.
  * @param  __HANDLE__: specifies the USART Handle
  * @param  __FLAG__: specifies the flag to check.
  *        This parameter can be one of the following values:
  *            @arg USART_FLAG_TXFT: TXFIFO threshold flag
  *            @arg USART_FLAG_RXFT: RXFIFO threshold flag
  *            @arg USART_FLAG_RXFF:  RXFIFO Full flag
  *            @arg USART_FLAG_TXFE:  TXFIFO Empty flag
  *            @arg USART_FLAG_REACK: Receive enable ackowledge flag
  *            @arg USART_FLAG_TEACK: Transmit enable ackowledge flag
  *            @arg USART_FLAG_BUSY:  Busy flag
  *            @arg USART_FLAG_TXE:   Transmit data register empty flag
  *            @arg USART_FLAG_TC:    Transmission Complete flag
  *            @arg USART_FLAG_RXNE:  Receive data register not empty flag
  *            @arg USART_FLAG_IDLE:  Idle Line detection flag
  *            @arg USART_FLAG_ORE:   OverRun Error flag
  *            @arg USART_FLAG_UDR:   UnderRun Error flag
  *            @arg USART_FLAG_NE:    Noise Error flag
  *            @arg USART_FLAG_FE:    Framing Error flag
  *            @arg USART_FLAG_PE:    Parity Error flag
  * @retval The new state of __FLAG__ (TRUE or FALSE).
  */
#define __HAL_USART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->ISR & (__FLAG__)) == (__FLAG__))

USART_FLAG有如下几种取值:

 

请大家重点关注上表中红字部分,USART标志是需要软件主动清零的。清零有两种方式:一种是调用__HAL_USART_CLEAR_FLAG函数,另一种是操作相关寄存器后自动清零。

/** @brief  Clear the specified USART pending flag.
  * @param  __HANDLE__: specifies the USART Handle.
  * @param  __FLAG__: specifies the flag to check.
  *          This parameter can be any combination of the following values:
  *            @arg USART_FLAG_TXFT:  TXFIFO threshold flag
  *            @arg USART_FLAG_RXFT:  RXFIFO threshold flag
  *            @arg USART_FLAG_RXFF:  RXFIFO Full flag
  *            @arg USART_FLAG_TXFE:  TXFIFO Empty flag
  *            @arg USART_FLAG_REACK: Receive enable ackowledge flag
  *            @arg USART_FLAG_TEACK: Transmit enable ackowledge flag
  *            @arg USART_FLAG_WUF:   Wake up from stop mode flag
  *            @arg USART_FLAG_RWU:   Receiver wake up flag (is the USART in mute mode)
  *            @arg USART_FLAG_SBKF:  Send Break flag
  *            @arg USART_FLAG_CMF:   Character match flag
  *            @arg USART_FLAG_BUSY:  Busy flag
  *            @arg USART_FLAG_ABRF:  Auto Baud rate detection flag
  *            @arg USART_FLAG_ABRE:  Auto Baud rate detection error flag
  *            @arg USART_FLAG_RTOF:  Receiver timeout flag
  *            @arg USART_FLAG_LBD:   LIN Break detection flag
  *            @arg USART_FLAG_TXE:   Transmit data register empty flag
  *            @arg USART_FLAG_TC:    Transmission Complete flag
  *            @arg USART_FLAG_RXNE:  Receive data register not empty flag
  *            @arg USART_FLAG_IDLE:  Idle Line detection flag
  *            @arg USART_FLAG_ORE:   OverRun Error flag
  *            @arg USART_FLAG_NE:    Noise Error flag
  *            @arg USART_FLAG_FE:    Framing Error flag
  *            @arg USART_FLAG_PE:    Parity Error flag
  * @retval The new state of __FLAG__ (TRUE or FALSE).
  */
#define __HAL_USART_CLEAR_FLAG(__HANDLE__, __FLAG__) ((__HANDLE__)->Instance->ICR = (__FLAG__))

上面介绍的USART标志大部分能够设置为产生中断,也就是有对应的USART中断标志。我们只介绍几个串口驱动要用到的中断标志:

USART_IT_TXETXE:发送数据寄存器空(此时数据可能正在发送)。

USART_IT_TC发送完成 。

USART_IT_RXNE接收数据寄存器非空。

中断缺省都是关闭的,通过__HAL_USART_ENABLE_IT函数可以使能相应的中断标志。函数定义如下:

/** @brief  Enable the specified USART interrupt.
  * @param  __HANDLE__: specifies the USART Handle.
  * @param  __INTERRUPT__: specifies the USART interrupt source to enable.
  *          This parameter can be one of the following values:
  *            @arg USART_IT_RXFF: RXFIFO Full interrupt
  *            @arg USART_IT_TXFE: TXFIFO Empty interrupt
  *            @arg USART_IT_RXFT: RXFIFO threshold interrupt
  *            @arg USART_IT_TXFT: TXFIFO threshold interrupt
  *            @arg USART_IT_TXE : Transmit Data Register empty interrupt
  *            @arg USART_IT_TC  : Transmission complete interrupt
  *            @arg USART_IT_RXNE: Receive Data register not empty interrupt
  *            @arg USART_IT_IDLE: Idle line detection interrupt
  *            @arg USART_IT_PE  : Parity Error interrupt
  *            @arg USART_IT_ERR : Error interrupt(Frame error, noise error, overrun error)
  * @retval None
  */
#define __HAL_USART_ENABLE_IT(__HANDLE__, __INTERRUPT__)   (((((uint8_t)(__INTERRUPT__)) >> 5U) == 1)? ((__HANDLE__)->Instance->CR1 |= (1U << ((__INTERRUPT__) & USART_IT_MASK))): \
                                                            ((((uint8_t)(__INTERRUPT__)) >> 5U) == 2)? ((__HANDLE__)->Instance->CR2 |= (1U << ((__INTERRUPT__) & USART_IT_MASK))): \
                          ((__HANDLE__)->Instance->CR3 |= (1U << ((__INTERRUPT__) & USART_IT_MASK))))

STM32一个串口的中断服务程序入口地址只有一个,进入中断服务程序后,我们需要判断是什么原因进入的中断,因此需要调用一个函数来检测中断标志。函数原型如下:

#define __HAL_USART_GET_IT(__HANDLE__, __IT__) ((__HANDLE__)->Instance->ISR & ((uint32_t)1 << ((__IT__)>> 0x08)))

中断处理完毕后,必须软件清除中断标志,否则中断返回后,会重入中断。清中断标志位的函数为:

#define __HAL_USART_CLEAR_IT(__HANDLE__, __IT_CLEAR__) ((__HANDLE__)->Instance->ICR = (uint32_t)(__IT_CLEAR__)) 

正如前面介绍的,不是所有的标志都需要用这个函数清零。

 

注意:操作串口的寄存器不限制必须要用HAL库提供的API,比如要操作寄存器CR1,直接调用USART1->CR1操作即可。

23.3.5 串口初始化流程总结

使用方法由HAL库提供:

  第1步:定义UART_HandleTypeDef类型串口结构体变量,比如UART_HandleTypeDef huart。

  第2步:使用函数HAL_UART_MspInit初始化串口底层,不限制一定要用此函数里面初始化,用户也可以自己实现。

  •   使能串口时钟。
  •   引脚配置。

        a、使能串口所使用的GPIO时钟。

        b、配置GPIO的复用模式。

  •   如果使用中断方式函数HAL_UART_Transmit_IT和HAL_UART_Receive_IT需要做如下配置。

        a、配置串口中断优先级。

        b、使能串口中断。

  •   串口中断的开关是通过函数__HAL_UART_ENABLE_IT() 和 __HAL_UART_DISABLE_IT()来实现,这两个函数被嵌套到串口的发送和接收函数中调用。
  •   如果使用DMA方式函数HAL_UART_Transmit_DMA和HAL_UART_Receive_DMA需要做如下配置。

        a、声明串口的发送和接收DMA结构体变量,注意发送和接收是独立的,如果都使用,那就都需要配置。

        b、使能DMA接口时钟。

        c、配置串口的发送和接收DMA结构体变量。

        d、配置DMA发送和接收通道。

        e、关联DMA和串口的句柄。

        f、配置发送DMA和接收DMA的传输完成中断和中断优先级。

  第3步:配置串口的波特率,位长,停止位,奇偶校验位,流控制和发送接收模式。

  第4步:串口初始化调用的函数HAL_UART_Init初始化。

23.4 源文件stm32f4xx_hal_uart.c

此文件涉及到的函数较多,这里把几个常用的函数做个说明:

  •   HAL_UART_Init
  •   HAL_UART_Transmit
  •   HAL_UART_Receive
  •   HAL_UART_Transmit_IT
  •   HAL_UART_Receive_IT
  •   HAL_UART_Transmit_DMA
  •   HAL_UART_Receive_DMA

 

其实V6开发板设计的8个串口FIFO驱动文件bsp_uart_fifo.c仅用到了函数HAL_UART_Init,其它函数都没有用到,不过这里也为大家做个说明。

23.4.1 函数HAL_UART_Init

函数原型:

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
{

   /* 省略 */

  if(huart->gState == HAL_UART_STATE_RESET)
  {
    huart->Lock = HAL_UNLOCKED;

    /* 初始化硬件: GPIO, CLOCK */
    HAL_UART_MspInit(huart);
  }

  huart->gState = HAL_UART_STATE_BUSY;

  /* 禁止串口 */
  __HAL_UART_DISABLE(huart);

  /* 配置串口参数 */
  if (UART_SetConfig(huart) == HAL_ERROR)
  {
    return HAL_ERROR;
  }

   /* 配置串口高级特性 */
  if (huart->AdvancedInit.AdvFeatureInit != UART_ADVFEATURE_NO_INIT)
  {
    UART_AdvFeatureConfig(huart);
  }

  /* 清寄存器的一些标志位 */
  CLEAR_BIT(huart->Instance->CR2, (USART_CR2_LINEN | USART_CR2_CLKEN));
  CLEAR_BIT(huart->Instance->CR3, (USART_CR3_SCEN | USART_CR3_HDSEL | USART_CR3_IREN));

  /* 使能串口 */
  __HAL_UART_ENABLE(huart);

  return (UART_CheckIdleState(huart));
}

函数描述:

此函数用于初始化串口的基础特性和高级特性。

函数参数:

  •   第1个参数是UART_HandleTypeDef类型结构体指针变量,用于配置要初始化的参数。
  •   返回值,返回HAL_TIMEOUT表示超时,HAL_ERROR表示参数错误,HAL_OK表示发送成功,HAL_BUSY表示串口忙,正在使用中。

注意事项:

  1. 函数HAL_UART_MspInit用于初始化USART的底层时钟、引脚等功能。需要用户自己在此函数里面实现具体的功能。由于这个函数是弱定义的,允许用户在工程其它源文件里面重新实现此函数。当然,不限制一定要在此函数里面实现,也可以像早期的标准库那样,用户自己初始化即可,更灵活些。
  2. 如果形参huart的结构体成员gState没有做初始状态,这个地方就是个坑。特别是用户搞了一个局部变量UART_HandleTypeDef UartHandle。

对于局部变量来说,这个参数就是一个随机值,如果是全局变量还好,一般MDK和IAR都会将全部变量初始化为0,而恰好这个 HAL_UART_STATE_RESET  = 0x00U。

解决办法有三

方法1:用户自己初始串口和涉及到的GPIO等。

方法2:定义UART_HandleTypeDef UartHandle为全局变量。

方法3:下面的方法

if(HAL_UART_DeInit(&UartHandle) != HAL_OK)
{
    Error_Handler();
}  
if(HAL_UART_Init(&UartHandle) != HAL_OK)
{
    Error_Handler();
}
  1. 注意串口的中断状态寄存器USART_ISR复位后,TC发送完成状态和RXNE接收状态都被置1,如果用户使能这两个中断前,最好优先清除中断标志。

使用举例:

UART_HandleTypeDef UartHandle;

/* USART3工作在UART模式 */
/* 配置如下:
  - 数据位 = 8 Bits
  - 停止位 = 1 bit
  - 奇偶校验位 = 无
  - 波特率 = 115200bsp
  - 硬件流控制 (RTS 和 CTS 信号) */
UartHandle.Instance        = USART3;

UartHandle.Init.BaudRate     = 115200;
UartHandle.Init.WordLength   = UART_WORDLENGTH_8B;
UartHandle.Init.StopBits     = UART_STOPBITS_1;
UartHandle.Init.Parity       = UART_PARITY_NONE;
UartHandle.Init.HwFlowCtl    = UART_HWCONTROL_NONE;
UartHandle.Init.Mode         = UART_MODE_TX_RX;
UartHandle.Init.OverSampling = UART_OVERSAMPLING_16;

if(HAL_UART_Init(&UartHandle) != HAL_OK)
{
    Error_Handler();
}

23.4.2 函数HAL_UART_Transmit

函数原型:

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
   /* 省略 */

  if(huart->gState == HAL_UART_STATE_READY)
  {
     /* 省略 */

/* 上锁 */
__HAL_LOCK(huart);

    while(huart->TxXferCount > 0U)
    {
huart->TxXferCount--;
/数据是9bit */
      if (huart->Init.WordLength == UART_WORDLENGTH_9B)
      {
        /* 等待发送空标志 */
        if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
        {
          return HAL_TIMEOUT;
        }
        tmp = (uint16_t *) pData;
        huart->Instance->DR = (*tmp & (uint16_t)0x01FF);
        if (huart->Init.Parity == UART_PARITY_NONE)
        {
          pData += 2U;
        }
        else
        {
          pData += 1U;
        }
      }
      /* 其它数据宽度 */
      else
      {
        /* 等待发送空标志 */
        if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
        {
          return HAL_TIMEOUT;
        }
        huart->Instance->DR = (*pData++ & (uint8_t)0xFF);
      }
    }

    /* 等待发送完成 */
    if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK)
    {
      return HAL_TIMEOUT;
    }

    /* 就绪 */
    huart->gState = HAL_UART_STATE_READY;

    /* 解锁 */
    __HAL_UNLOCK(huart);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

函数描述:

此函数以查询的方式发送指定字节。看源码的话,程序里面最重要的就是上面代码中置红的两个标志,发送空标志和发送完成标志。发送空标志表示发送数据寄存器为空,数据还在移位寄存器里面,而发送完成标志表示数据已经从移位寄存器发送出去。

函数参数:

  •   第1个参数是UART_HandleTypeDef类型结构体指针变量。
  •   第2个参数是要发送的数据地址。
  •   第3个参数是要发送的数据大小,单位字节。
  •   第4个参数是溢出时间,单位ms。
  •   返回值,返回HAL_TIMEOUT表示超时,HAL_ERROR表示参数错误,HAL_OK表示发送成功,HAL_BUSY表示串口忙,正在使用中。

使用举例:

/*
*********************************************************************************************************
*    函 数 名: fputc
*    功能说明: 重定义putc函数,这样可以使用printf函数从串口1打印输出
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&UartHandle, (uint8_t *)&ch, 1, HAL_MAX_DELAY);

    return ch;
}

23.4.3 函数HAL_UART_Receive

函数原型:

HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
  uint16_t *tmp;
  uint32_t tickstart = 0U;

  /* 检测RX接收状态就绪 */
  if (huart->RxState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return  HAL_ERROR;
    }

    /* 上锁 */
    __HAL_LOCK(huart);

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* 获取当前的计数 */
    tickstart = HAL_GetTick();

    huart->RxXferSize = Size;
    huart->RxXferCount = Size;

    /* 阻塞式接收数据 */
    while (huart->RxXferCount > 0U)
    {
      huart->RxXferCount--;
      /* 接收的数据是9bit的 */
if (huart->Init.WordLength == UART_WORDLENGTH_9B)
      {
        if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK)
        {
          return HAL_TIMEOUT;
        }
        tmp = (uint16_t *) pData;
        if (huart->Init.Parity == UART_PARITY_NONE)
        {
          *tmp = (uint16_t)(huart->Instance->DR & (uint16_t)0x01FF);
          pData += 2U;
        }
        else
        {
          *tmp = (uint16_t)(huart->Instance->DR & (uint16_t)0x00FF);
          pData += 1U;
        }
      }
      /* 其它bit接收 */
      else
      {
        if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK)
        {
          return HAL_TIMEOUT;
        }
        if (huart->Init.Parity == UART_PARITY_NONE)
        {
          *pData++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
        }
        else
        {
          *pData++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
        }

      }
    }

    /* 接收就绪 At end of Rx process, restore huart->RxState to Ready */
    huart->RxState = HAL_UART_STATE_READY;

    /* 解锁 */
    __HAL_UNLOCK(huart);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

函数描述:

此函数以查询的方式接收指定字节。这个函数相对比较好理解,就是等待上面程序中的RXNE标志,置位了表示接收数据寄存器已经存入数据。

函数参数:

  •   第1个参数是UART_HandleTypeDef类型结构体指针变量。
  •   第2个参数是要接收的数据地址。
  •   第3个参数是要接收的数据大小,单位字节。
  •   第4个参数是溢出时间,单位ms。
  •   返回值,返回HAL_TIMEOUT表示超时,HAL_ERROR表示参数错误,HAL_OK表示发送成功,HAL_BUSY表示串口忙,正在使用中。

使用举例:

/*
*********************************************************************************************************
*    函 数 名: fgetc
*    功能说明: 重定义getc函数,这样可以使用scanff函数从串口1输入数据
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
int fgetc(FILE *f)
{
    int ret;
        
    HAL_UART_Receive(&UartHandle, (uint8_t *)&ret, 1, HAL_MAX_DELAY);

    return ret;
}

23.4.4 函数HAL_UART_Transmit_IT

函数原型:

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  /* 检测是否处于就绪状态 */
  if (huart->gState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* 上锁 */
    __HAL_LOCK(huart);

    huart->pTxBuffPtr = pData;
    huart->TxXferSize = Size;
    huart->TxXferCount = Size;

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->gState = HAL_UART_STATE_BUSY_TX;

    /* 解锁 */
    __HAL_UNLOCK(huart);

    /* 使能发送空中断 */
    __HAL_UART_ENABLE_IT(huart, UART_IT_TXE);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

函数描述:

此函数以中断的方式发送指定字节,可以选择使能FIFO中断方式或者发送空中断方式。具体数据的发送是在中断处理函数HAL_UART_IRQHandler里面实现。

函数参数:

  •   第1个参数是UART_HandleTypeDef类型结构体指针变量。
  •   第2个参数是要发送的数据地址。
  •   第3个参数是要发送的数据大小,单位字节。
  •   返回值,返回HAL_ERROR表示参数错误,HAL_OK表示发送成功,HAL_BUSY表示串口忙,正在使用中。

使用举例:

使用中断方式要使能串口中断,此贴有完整例子:

http://www.armbbs.cn/forum.php?mod=viewthread&tid=86245

UART_HandleTypeDef UartHandle;
uint8_t s_ucBuf[5];

/* 数据发送 */
HAL_UART_Transmit_IT(&UartHandle, s_ucBuf, 1);
HAL_UART_Transmit_IT(&UartHandle, (uint8_t*)"KEY_DOWN_K1\r\n", 13);

23.4.5 函数HAL_UART_Receive_IT

函数原型:

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
/* 检查是否处于就绪态 */
  if (huart->RxState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* 上锁 */
    __HAL_LOCK(huart);

    huart->pRxBuffPtr = pData;
    huart->RxXferSize = Size;
    huart->RxXferCount = Size;

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* 解锁 */
    __HAL_UNLOCK(huart);

    /* 使能校验错误中断 */
    __HAL_UART_ENABLE_IT(huart, UART_IT_PE);

    /* 使能帧错误,噪声错误和溢出中断 */
    __HAL_UART_ENABLE_IT(huart, UART_IT_ERR);

    /* 使能接收非空中断 ,即接收中断 /
    __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

函数描述:

此函数以中断的方式接收指定字节,使能了奇偶校验中断失败和错误中断。具体数据的接收是在中断处理函数HAL_UART_IRQHandler里面实现。

函数参数:

  •   第1个参数是UART_HandleTypeDef类型结构体指针变量。
  •   第2个参数是要接收的数据地址。
  •   第3个参数是要接收的数据大小,单位字节。
  •   返回值,返回HAL_ERROR表示参数错误,HAL_OK表示发送成功,HAL_BUSY表示串口忙,正在使用中。

使用举例:

使用中断方式要使能串口中断,此贴有完整例子:

http://www.armbbs.cn/forum.php?mod=viewthread&tid=86245

UART_HandleTypeDef UartHandle;
uint8_t s_ucBuf[5];

/* 数据接收*/
HAL_UART_Receive_IT(&UartHandle, s_ucBuf, 1);

23.4.6 函数HAL_UART_Transmit_DMA

函数原型:

HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
uint32_t *tmp;

/* 检查是否处于就绪态 */
  if (huart->gState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* 上锁 */
    __HAL_LOCK(huart);

    huart->pTxBuffPtr = pData;
    huart->TxXferSize = Size;
    huart->TxXferCount = Size;

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->gState = HAL_UART_STATE_BUSY_TX;

    /* 设置串口DMA传输完成回调 */
    huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt;

    /* 设置串口传输完成回调 */
    huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt;

    /* 设置DMA错误回调 */
    huart->hdmatx->XferErrorCallback = UART_DMAError;

    /* 设置DMA终止回调 */
    huart->hdmatx->XferAbortCallback = NULL;

    /* 使能串口DMA方式发送 */
    tmp = (uint32_t *)&pData;
    HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);

    /* 清除TC标志 */
    __HAL_UART_CLEAR_FLAG(huart, UART_FLAG_TC);

    /* 解锁 */
    __HAL_UNLOCK(huart);

    /* 使能串口DMA */
    SET_BIT(huart->Instance->CR3, USART_CR3_DMAT);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

函数描述:

此函数以DMA的方式发送指定字节。这里是用的DMA中断方式HAL_DMA_Start_IT进行的发送。所以使用此函数的话,不要忘了写DMA中断服务程序。而且DMA的配置也是需要用户实现的,可以直接在函数HAL_UART_MspInit里面实现,也可以放在其它位置。

函数参数:

  •   第1个参数是UART_HandleTypeDef类型结构体指针变量。
  •   第2个参数是要发送的数据地址。
  •   第3个参数是要发送的数据大小,单位字节。
  •   返回值,返回HAL_ERROR表示参数错误,HAL_OK表示发送成功,HAL_BUSY表示串口忙,正在使用中。

使用举例:

使用DMA方式,此贴有完整例子:

http://www.armbbs.cn/forum.php?mod=viewthread&tid=86271

23.4.7 函数HAL_UART_Receive_DMA

函数原型:

HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  uint32_t *tmp;

/* 检查是否处于就绪态 */
  if (huart->RxState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* 上锁 */
    __HAL_LOCK(huart);

    huart->pRxBuffPtr = pData;
    huart->RxXferSize = Size;

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* 设置串口DMA接收完成回调函数 */
    huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;

    /* 设置串口半传输完成回调函数 */
    huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt;

    /* 设置DMA错误回调 */
    huart->hdmarx->XferErrorCallback = UART_DMAError;

    /* 设置DMA终止传输回调 */
    huart->hdmarx->XferAbortCallback = NULL;

    /* 使能串口DMA Enable the DMA stream */
    tmp = (uint32_t *)&pData;
    HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);

    /*在启用DMA Rx请求之前清除溢出标志:可以强制第2次传输 */
    __HAL_UART_CLEAR_OREFLAG(huart);

    /* 上锁 */
    __HAL_UNLOCK(huart);

    /* 使能校验错误中断 */
    SET_BIT(huart->Instance->CR1, USART_CR1_PEIE);

    /* 使能帧错误,噪声错误和溢出中断 */
    SET_BIT(huart->Instance->CR3, USART_CR3_EIE);

    /* 使能串口DMA */
    SET_BIT(huart->Instance->CR3, USART_CR3_DMAR);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

函数描述:

此函数以DMA的方式接收指定字节。这里是用的DMA中断方式HAL_DMA_Start_IT进行的接收。所以使用此函数的话,不要忘了写DMA中断服务程序。而且DMA的配置也是需要用户实现的,可以直接在函数HAL_UART_MspInit里面实现,也可以放在其它位置。

函数参数:

  •   第1个参数是UART_HandleTypeDef类型结构体指针变量。
  •   第2个参数是要接收的数据地址。
  •   第3个参数是要接收的数据大小,单位字节。
  •   返回值,返回HAL_ERROR表示参数错误,HAL_OK表示发送成功,HAL_BUSY表示串口忙,正在使用中。

使用举例:

使用DMA方式,此贴有完整例子:

http://www.armbbs.cn/forum.php?mod=viewthread&tid=86271

23.5 总结

本章节就为大家讲解这么多,涉及到的知识点和API函数比较多,需要花点时间消化,后面用到的多了,就可以熟练掌握了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值