STM32之CubeL4(二)---USART + DMA + HAL

一、UART简介

1.1 串口通信简介

一般情况下,设备之间的通信方式可以分成并行通信和串行通信两种,它们的区别是:

通信方式 并行通信 串行通信
传输原理 数据各个位同时传输 数据按位顺序传输
优点 传输速度快 占用引脚资源少
缺点 占用引脚资源多 传输速度相对较慢

在集成电路芯片的内部、同一硬件板上各部件之间等(比如指令总线、数据总线、系统总线等)的数据传送通常采用并行通信方式;硬件板与外接设备间为了减少引脚占用常采用串行通信方式,这里我们重点关注开发板与外界设备的通信,故继续看串行通信的分类:

串行通信 分类 特点
工作模式 单工模式
半双工模式
全双工模式
通讯双方一方为发送端,另一方则固定为接收端,数据只能单向传输;
通讯双方既可以发射也可以接收,但是接收和发射不能同时进行;
双方既可以发射也可以接收,且能在同一时刻进行发送和接收操作;
同步方式 同步通信
异步通信
带时钟同步信号传输,双方靠时钟同步,比如:SPI,IIC通信接口;
不带时钟同步信号,双方需规约好传输速率,比如:UART;

在同步通讯中,数据信号所传输的内容绝大部分是有效数据,而异步通讯中会则会包含数据帧的各种标识符(用于数据同步的信号位,比如起始位、停止位、奇偶校验位等),所以同步通讯效率高,但是同步通讯双方的时钟允许误差小,稍稍时钟出错就可能导致数据错乱,异步通讯双方的时钟允许误差较大。

常见的串行通信接口汇总如下:

通信协议 引脚说明 工作模式 同步方式
UART TXD:发送端
RXT:接收端
GND:共地
全双工 异步通信
SPI SCK:同步时钟
MISO:主机输入,从机输出
MOSI:主机输出,从机输入
全双工 同步通信
I2C / IIC SCK:同步时钟
SDA:数据输入/输出端
半双工 同步通信

1.2 USART简介

USART全称为Universal Synchronous Asynchronous Receiver and Transmitter,通用同步-异步接收发射器,怎么既同步又异步呢?实际上USART既支持同步通信也支持异步通信,如果作为异步通信就是UART(Universal Asynchronous Receiver and Transmitter),作为同步通信相比UART多了一根时钟同步信号线,USART相当于UART的增强版,二者的关系如下:
USART与UART区别
由上图可以看出USART以同步方式通信需要时钟同步信号,但不需要额外的起始、停止位,可以实现更快的传输速度。但USART的支持范围更有限,后面我们主要介绍其作为异步通信也即UART通信。

串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备的RXD接口,通讯双方的数据包格式要规约一致才能正常收发数据。UART串口通信的数据包以帧为单位,常用的帧结构为:1位起始位+8位数据位+1位奇偶校验位(可选)+1位停止位,如下图所示:
串口异步通讯格式帧
奇偶校验位分为奇校验和偶校验两种,是一种简单的数据误码校验方法。奇校验是指每帧数据中,包括数据位和奇偶校验位的全部9个位中1的个数必须为奇数;偶校验是指每帧数据中,包括数据位和奇偶校验位的全部9个位中1的个数必须为偶数。

STM32L475中USART结构框图如下图所示:
STM32L475 USART结构框图
这个框图分成上、中、下三个部分。本文大概讲述一下各个部分的内容,具体的可以看《SMT32L475VE Reference manual》中的描述。

框图的上部分,数据从RX进入到接收移位寄存器,后进入到接收数据寄存器,最终供CPU或者DMA来进行读取;数据从CPU或者DMA传递过来,进入发送数据寄存器,后进入发送移位寄存器,最终通过TX发送出去(感觉上面框图箭头有误)。

框图的中间部分,UART的发送移位寄存器和接收移位寄存器分别由发送控制器和接收控制器来进行控制,而发送控制器和接收控制器主要受硬件流控信号和时钟信号控制。硬件流控nRTS / nCTS (Request To Send / Clear To Send,n表示低电平有效)可以控制数据发送、接收的速率,防止因数据缓冲区溢出而导致部分数据丢失的情况出现,在很多情况下为了减少引脚数量,都省去了硬件流控引脚而采用软件流控方式以达到同样的目的。针对USART的同步通信方式还有一个发送器时钟输出引脚CK,仅适用于同步模式。

框图的下部分,接收控制器、发送控制器都有一个进入的箭头,分别连接到接收器时钟、发送器时钟。也就是说,异步通信尽管没有时钟同步信号,但是在串口内部,是提供了时钟信号来进行控制的。接收器时钟和发送器时钟又被连接到同一个控制单元,也就是说它们共用一个波特率发生器。

上面提到了波特率,讲波特率之前首先了解一下通讯速率,通讯速率通常是以比特率来表示,即每秒钟传输的二进制位数,单位为比特每秒(bit/s)。容易和比特率混淆的概念是“波特率”,它表示每秒传输了多少码元。码元是通讯信号调制的概念,时间间隔相同的符号来表示一个二进制数字,这样的信号就称为码元。如常见的通讯传输中,用0V表示数字0,5V表示数字1,那么一个码元可以表示两种状态0和1,所以一个码元等于一个二进制比特位,此时波特率的大小与比特率一致;若传输中,有0V、2V、4V和6V分别表示00、01、10、11,那么每个码元可以表示四种状态,两个二进制比特位,所以码元数是二进制比特位数的一半,这个时候的波特率为比特率的一半。因为很多常见的通讯中一个码元都是表示两种状态,人们常常直接以波特率来表示比特率,其实二者是有区别的。 异步通讯由于没有时钟信号,所以两个通讯设备需要规约好波特率,即每个码元的长度,以便对信号进行解码,常见的波特率为4800,9600,115200。

扫描二维码关注公众号,回复: 10622452 查看本文章

我们的STM32L475开发板支持的串口模式及特征如下:
STM32L475 USART模式特征

1.3 USART配置

前面介绍了UART数据帧的格式和波特率的概念,UART的初始化主要是确定数据帧格式和双方通信的波特率,下面先介绍UART初始化结构体的定义:

// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_uart.h
/**
  * @brief UART Init Structure definition
  */
typedef struct
{
  uint32_t BaudRate;                  /*!< This member configures the UART communication baud rate.
                                           The baud rate register is computed using the following formula:
                                           LPUART:
                                           =======
                                              Baud Rate Register = ((256 * lpuart_ker_ckpres) / ((huart->Init.BaudRate)))
                                           where lpuart_ker_ck_pres is the UART input clock (divided by a prescaler if applicable)
                                           UART:
                                           =====
                                           - If oversampling is 16 or in LIN mode,
                                              Baud Rate Register = ((uart_ker_ckpres) / ((huart->Init.BaudRate)))
                                           - If oversampling is 8,
                                              Baud Rate Register[15:4] = ((2 * uart_ker_ckpres) / ((huart->Init.BaudRate)))[15:4]
                                              Baud Rate Register[3] =  0
                                              Baud Rate Register[2:0] =  (((2 * uart_ker_ckpres) / ((huart->Init.BaudRate)))[3:0]) >> 1
                                           where uart_ker_ck_pres is the UART input clock (divided by a prescaler if applicable) */

  uint32_t WordLength;                /*!< Specifies the number of data bits transmitted or received in a frame.
                                           This parameter can be a value of @ref UARTEx_Word_Length. */

  uint32_t StopBits;                  /*!< Specifies the number of stop bits transmitted.
                                           This parameter can be a value of @ref UART_Stop_Bits. */

  uint32_t Parity;                    /*!< Specifies the parity mode.
                                           This parameter can be a value of @ref UART_Parity
                                           @note When parity is enabled, the computed parity is inserted
                                                 at the MSB position of the transmitted data (9th bit when
                                                 the word length is set to 9 data bits; 8th bit when the
                                                 word length is set to 8 data bits). */

  uint32_t Mode;                      /*!< Specifies whether the Receive or Transmit mode is enabled or disabled.
                                           This parameter can be a value of @ref UART_Mode. */

  uint32_t HwFlowCtl;                 /*!< Specifies whether the hardware flow control mode is enabled
                                           or disabled.
                                           This parameter can be a value of @ref UART_Hardware_Flow_Control. */

  uint32_t OverSampling;              /*!< Specifies whether the Over sampling 8 is enabled or disabled, to achieve higher speed (up to f_PCLK/8).
                                           This parameter can be a value of @ref UART_Over_Sampling. */

  uint32_t OneBitSampling;            /*!< Specifies whether a single sample or three samples' majority vote is selected.
                                           Selecting the single sample method increases the receiver tolerance to clock
                                           deviations. This parameter can be a value of @ref UART_OneBit_Sampling. */

#if defined(USART_PRESC_PRESCALER)
  uint32_t ClockPrescaler;            /*!< Specifies the prescaler value used to divide the UART clock source.
                                           This parameter can be a value of @ref UART_ClockPrescaler. */
#endif /* USART_PRESC_PRESCALER */

} UART_InitTypeDef;

/** @defgroup UART_Stop_Bits   UART Number of Stop Bits
  */
#define UART_STOPBITS_0_5                    USART_CR2_STOP_0                     /*!< UART frame with 0.5 stop bit  */
#define UART_STOPBITS_1                     0x00000000U                           /*!< UART frame with 1 stop bit    */
#define UART_STOPBITS_1_5                   (USART_CR2_STOP_0 | USART_CR2_STOP_1) /*!< UART frame with 1.5 stop bits */
#define UART_STOPBITS_2                      USART_CR2_STOP_1                     /*!< UART frame with 2 stop bits   */

/** @defgroup UART_Parity  UART Parity
  */
#define UART_PARITY_NONE                    0x00000000U                        /*!< No parity   */
#define UART_PARITY_EVEN                    USART_CR1_PCE                      /*!< Even parity */
#define UART_PARITY_ODD                     (USART_CR1_PCE | USART_CR1_PS)     /*!< Odd parity  

/** @defgroup UART_Mode UART Transfer Mode
  */
#define UART_MODE_RX                        USART_CR1_RE                    /*!< RX mode        */
#define UART_MODE_TX                        USART_CR1_TE                    /*!< TX mode        */
#define UART_MODE_TX_RX                     (USART_CR1_TE |USART_CR1_RE)    /*!< RX and TX mode */

/** @defgroup UART_Hardware_Flow_Control UART Hardware Flow Control
  */
#define UART_HWCONTROL_NONE                  0x00000000U                          /*!< No hardware control       */
#define UART_HWCONTROL_RTS                   USART_CR3_RTSE                       /*!< Request To Send           */
#define UART_HWCONTROL_CTS                   USART_CR3_CTSE                       /*!< Clear To Send             */
#define UART_HWCONTROL_RTS_CTS               (USART_CR3_RTSE | USART_CR3_CTSE)    /*!< Request and Clear To Send */

/** @defgroup UART_Over_Sampling UART Over Sampling
  */
#define UART_OVERSAMPLING_16                0x00000000U         /*!< Oversampling by 16 */
#define UART_OVERSAMPLING_8                 USART_CR1_OVER8     /*!< Oversampling by 8  */

/** @defgroup UART_OneBit_Sampling UART One Bit Sampling Method
  */
#define UART_ONE_BIT_SAMPLE_DISABLE         0x00000000U         /*!< One-bit sampling disable */
#define UART_ONE_BIT_SAMPLE_ENABLE          USART_CR3_ONEBIT    /*!< One-bit sampling enable  */

UART初始化结构体UART_InitTypeDef中的成员配置在CubeMX配置中会涉及,但多数可以使用默认配置,如果默认配置满足不了需求,则需要了解结构体成员有哪些配置选项,各配置选项有何意义。

前面介绍HAL库提供了更高的抽象,将每类外设抽象为一个句柄,对实例化句柄的操作相当于对该外设的操作,这就类似于C++中面向对象编程的思想。前面介绍GPIO更偏底层,只提供了初始化结构体,不需要句柄结构体,所以并没有介绍句柄结构体是怎样实现的?下面以UART为例介绍其句柄结构体定义如下:

// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_uart.h
/**
  * @brief  UART handle Structure definition
  */
typedef struct __UART_HandleTypeDef
{
  USART_TypeDef            *Instance;                /*!< UART registers base address        */

  UART_InitTypeDef         Init;                     /*!< UART communication parameters      */

  UART_AdvFeatureInitTypeDef AdvancedInit;           /*!< UART Advanced Features initialization 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           */

  uint16_t                 Mask;                     /*!< UART Rx RDR register mask          */

#if defined(USART_CR1_FIFOEN)
  uint32_t                 FifoMode;                 /*!< Specifies if the FIFO mode is being used.
                                                          This parameter can be a value of @ref UARTEx_FIFO_mode. */

  uint16_t                 NbRxDataToProcess;        /*!< Number of data to process during RX ISR execution */

  uint16_t                 NbTxDataToProcess;        /*!< Number of data to process during TX ISR execution */
#endif /*USART_CR1_FIFOEN */

  void (*RxISR)(struct __UART_HandleTypeDef *huart); /*!< Function pointer on Rx IRQ handler   */

  void (*TxISR)(struct __UART_HandleTypeDef *huart); /*!< Function pointer on Tx IRQ handler   */

  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);        /*!< UART Tx Half Complete Callback        */
  void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Tx Complete Callback             */
  void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);        /*!< UART Rx Half Complete Callback        */
  void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Rx Complete Callback             */
  void (* ErrorCallback)(struct __UART_HandleTypeDef *huart);             /*!< UART Error Callback                   */
  void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart);         /*!< UART Abort Complete Callback          */
  void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Transmit Complete Callback */
  void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart);  /*!< UART Abort Receive Complete Callback  */
  void (* WakeupCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Wakeup Callback                  */
#if defined(USART_CR1_FIFOEN)
  void (* RxFifoFullCallback)(struct __UART_HandleTypeDef *huart);        /*!< UART Rx Fifo Full Callback            */
  void (* TxFifoEmptyCallback)(struct __UART_HandleTypeDef *huart);       /*!< UART Tx Fifo Empty Callback           */
#endif /* USART_CR1_FIFOEN */

  void (* MspInitCallback)(struct __UART_HandleTypeDef *huart);           /*!< UART Msp Init callback                */
  void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart);         /*!< UART Msp DeInit callback              */
#endif  /* USE_HAL_UART_REGISTER_CALLBACKS */

} UART_HandleTypeDef;


/** @addtogroup UART_Exported_Functions_Group1 Initialization and de-initialization functions
  */
/* Initialization and de-initialization functions  ****************************/
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_HalfDuplex_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_LIN_Init(UART_HandleTypeDef *huart, uint32_t BreakDetectLength);
HAL_StatusTypeDef HAL_MultiProcessor_Init(UART_HandleTypeDef *huart, uint8_t Address, uint32_t WakeUpMethod);
HAL_StatusTypeDef HAL_UART_DeInit(UART_HandleTypeDef *huart);
void HAL_UART_MspInit(UART_HandleTypeDef *huart);
void HAL_UART_MspDeInit(UART_HandleTypeDef *huart);

/* Callbacks Register/UnRegister functions  ***********************************/
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
HAL_StatusTypeDef HAL_UART_RegisterCallback(UART_HandleTypeDef *huart, HAL_UART_CallbackIDTypeDef CallbackID,
                                            pUART_CallbackTypeDef pCallback);
HAL_StatusTypeDef HAL_UART_UnRegisterCallback(UART_HandleTypeDef *huart, HAL_UART_CallbackIDTypeDef CallbackID);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */


/** @addtogroup UART_Exported_Functions_Group2 IO operation functions
  */
/* IO operation functions *****************************************************/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);
/* Transfer Abort functions */
HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_AbortTransmit(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_AbortReceive(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_Abort_IT(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_AbortTransmit_IT(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_AbortReceive_IT(UART_HandleTypeDef *huart);

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);
void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart);


/** @addtogroup UART_Exported_Functions_Group3 Peripheral Control functions
  */
/* Peripheral Control functions  ************************************************/
HAL_StatusTypeDef HAL_LIN_SendBreak(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_MultiProcessor_EnableMuteMode(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_MultiProcessor_DisableMuteMode(UART_HandleTypeDef *huart);
void HAL_MultiProcessor_EnterMuteMode(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_HalfDuplex_EnableTransmitter(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_HalfDuplex_EnableReceiver(UART_HandleTypeDef *huart);


/** @addtogroup UART_Exported_Functions_Group4 Peripheral State and Error functions
  */
/* Peripheral State and Errors functions  **************************************************/
HAL_UART_StateTypeDef HAL_UART_GetState(UART_HandleTypeDef *huart);
uint32_t              HAL_UART_GetError(UART_HandleTypeDef *huart);


/* Private functions -----------------------------------------------------------*/
/** @addtogroup UART_Private_Functions UART Private Functions
  */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
void UART_InitCallbacksToDefault(UART_HandleTypeDef *huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
HAL_StatusTypeDef UART_SetConfig(UART_HandleTypeDef *huart);
HAL_StatusTypeDef UART_CheckIdleState(UART_HandleTypeDef *huart);
HAL_StatusTypeDef UART_WaitOnFlagUntilTimeout(UART_HandleTypeDef *huart, uint32_t Flag, FlagStatus Status,
                                              uint32_t Tickstart, uint32_t Timeout);
void UART_AdvFeatureConfig(UART_HandleTypeDef *huart);

UART句柄结构体UART_HandleTypeDef中的成员比较全面,既包括寄存器基地址结构体USART_TypeDef、UART初始化结构体UART_InitTypeDef,又包括Tx / Rx缓冲区地址及大小、中断处理函数指针、DMA句柄指针、回调函数指针等,基本可以通过UART句柄访问与UART有关的所有资源,因此HAL库为UART提供的API函数基本都以UART句柄作为参数传入。

正如前一篇HAL库详解中介绍的,UART的API函数主要可分为如下几类:

API分类 API函数
初始化/反初始化函数 HAL_UART_Init() / HAL_UART_DeInit();
HAL_UART_MspInit() / HAL_UART_MspDeInit();
I / O 操作函数 HAL_UART_Transmit() / HAL_UART_Receive();
HAL_UART_Transmit_IT() / HAL_UART_Receive_IT();
HAL_UART_Transmit_DMA() / HAL_UART_Receive_DMA();
回调函数 HAL_UART_TxCpltCallback() / HAL_UART_RxCpltCallback();
HAL_UART_TxHalfCpltCallback() / HAL_UART_RxHalfCpltCallback();
HAL_UART_ErrorCallback() / HAL_UART_AbortCpltCallback();
HAL_UART_RegisterCallback() / HAL_UART_UnRegisterCallback();
控制函数 UART_SetConfig() / UART_AdvFeatureConfig();
状态与错误 HAL_UART_GetState() / HAL_UART_GetError();

二、UART使用之中断

前面介绍了UART的结构、原理,了解了UART常用的API,可以开始UART程序开发了。UART包括其它外设,通常有三种I / O方式,分别为轮询、中断和DMA,下面先以最常见的中断为例展示UART程序开发的一般步骤。

2.1 CubeMX配置UART1

我们在前一篇展示的RGB_LED工程的基础上新增UART1配置,USART1的GPIO引脚配置(GPIO参数按默认配置即可)如下:
USART1的GPIO引脚配置
USART1初始化参数配置如下(默认配置即可):
USART1初始化参数配置
USART1的NVIC中断配置(包括中断优先级分组、抢占优先级、子优先级、使能中断等)如下:
USART1的NVIC配置
由于我们的USART1只使用中断,并没有使用DMA,所以DMA不需要配置。到这里USART1的配置完成,生成代码后到KEIL MDK V5工程中继续新增用户控制逻辑和回调函数。

2.2 完成用户控制逻辑

前面已经介绍过USART HAL库的API函数,其中初始化与反初始化函数已经由CubeMX生成了,我们在用户控制逻辑中主要实现的是I / O函数与处理完成回调函数,由于这里是采用中断方式接收/发送,所以我们主要用到的HAL API如下:

// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_uart.h

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

由上面的函数声明可知,UART接收/发送函数需要指定数据长度,当达到指定的数据长度后触发中断,但我们往往不知道将要接收到的数据长度是多少?所以我们不能依赖HAL_UART_Receive_IT直接接收一段数据,常需要在接受完成回调函数中设置接收完成标志位(常把收到回车换行符作为该段数据结束的标志),在用户控制逻辑中只有通过该标志位确定该段数据接收完成后才对其进行处理。

要发送/接收数据,一般需要相应的缓冲区暂存数据,同时需要知道数据长度,接收/发送完成标志位等元素,我们可以把这些变量组织成一个结构体变量统一管理,我们定义的数据结构如下:

// Core\Inc\main.h

/* USER CODE BEGIN Private defines */
#define USART1_TX_LEN		256
#define USART1_RX_LEN		256

typedef struct{
	uint8_t			TxBuff[USART1_TX_LEN];
	uint16_t		TxSize;
	uint8_t			Tx_end_flag;
	uint8_t			RxBuff[USART1_RX_LEN];
  uint8_t			aRxBuff;
	uint16_t		RxSize;
	uint8_t			Rx_end_flag;
}USART_BuffTypeDef;

extern USART_BuffTypeDef usart1_buf;

/* USER CODE END Private defines */


// main.c

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
USART_BuffTypeDef usart1_buf;

/* USER CODE END PV */

USART_BuffTypeDef结构定义在main.h中,初始化结构体变量usart1_buf在main.c中,usart1_buf作为全局变量,在系统启动时默认初始化为0,所以这里可以省略赋初值。

接下来完成接收/发送回调函数的编写,在接收回调函数中把回车换行符/r或/n作为一段数据的结束标志符,检测到结束标识符则结束标志置位,否则将该字符存放到接收数据缓冲区内;在发送回调函数中返回已发送数据段的字符总数,最后清空发送缓冲区和标志位,两个回调函数的实现代码如下:

// Core\Src\usart.c

/* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if(huart->Instance == USART1)
  {
		if(usart1_buf.aRxBuff == '\n' || usart1_buf.aRxBuff == '\r'){
			usart1_buf.Rx_end_flag = 1;
			usart1_buf.RxBuff[usart1_buf.RxSize] = '\0';
		}else{
			usart1_buf.RxBuff[usart1_buf.RxSize] = usart1_buf.aRxBuff;
			usart1_buf.RxSize++;
		}
		HAL_UART_Receive_IT(huart, &usart1_buf.aRxBuff, 1);
	}
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance == USART1)
    {
	    if(usart1_buf.Tx_end_flag == 0){
	      	sprintf((char *)usart1_buf.TxBuff,"\r\n发送完成,已发送字符个数为:%d\r\n",usart1_buf.TxSize);
			usart1_buf.TxSize = strlen((char *)usart1_buf.TxBuff);
	      
		    if(HAL_UART_Transmit(huart, usart1_buf.TxBuff, usart1_buf.TxSize, 0xffff) != HAL_OK){
			  	Error_Handler();
		    }
			usart1_buf.TxSize = 0;
	        usart1_buf.Tx_end_flag = 1;
		    memset(usart1_buf.TxBuff,0,usart1_buf.TxSize);
	    }
	}
}

上面接收回调函数中再次调用了接收中断函数HAL_UART_Receive_IT,主要是HAL在响应接收中断后失能了接收中断,如果想继续接收数据,需要通过HAL_UART_Receive_IT再次使能接收中断;同时,USART1初始化后并没有使能接收中断,所以我们在接收数据前也需要调用HAL_UART_Receive_IT以使能接收中断。

接下来我们在main函数中完成控制逻辑,我们想实现的功能是:待接收完一段数据后,将这段数据的长度和内容发送给USART1,然后清空接收缓冲区和标志位,同时我们通过接收到的数据长度控制RGB LED按相应的颜色闪烁。为了有更好的提示效果,我们每隔一段时间发送一次提示信息,该部分的实现代码如下:

// Core\Src\main.c

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
	uint16_t times = 0;
	uint16_t colors = BLACK;
  /* USER CODE END 1 */
	......
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
	
	if(HAL_UART_Receive_IT(&huart1, &usart1_buf.aRxBuff, 1) != HAL_OK){
		Error_Handler();
	}
	
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		if(usart1_buf.Rx_end_flag == 1){
			sprintf((char *)usart1_buf.TxBuff,"\r\n接收到的字符个数为:%d\r\n接收到的数据为:%s\r\n",usart1_buf.RxSize, usart1_buf.RxBuff);
			usart1_buf.TxSize = strlen((char *)usart1_buf.TxBuff);
			usart1_buf.Tx_end_flag = 0;

			if(HAL_UART_Transmit_IT(&huart1, usart1_buf.TxBuff, usart1_buf.TxSize) != HAL_OK){
				Error_Handler();
			}
      		colors = usart1_buf.RxSize;
			usart1_buf.RxSize = 0;
			usart1_buf.Rx_end_flag = 0;
			memset(usart1_buf.RxBuff,0,usart1_buf.RxSize);
		}else{
			
			if(times % 1000 == 0){
				sprintf((char *)usart1_buf.TxBuff,"\r\n请输入数据,以回车键结束:\r\n");
				usart1_buf.TxSize = strlen((char *)usart1_buf.TxBuff);
        		usart1_buf.Tx_end_flag = 1;
        
				if(HAL_UART_Transmit_IT(&huart1, usart1_buf.TxBuff, usart1_buf.TxSize) != HAL_OK){
					Error_Handler();
				}
			}
			
			RGB_ON((times/100)%2 ? colors : BLACK);
			
			times++;
			
			HAL_Delay(10);
		}		
  }
  /* USER CODE END 3 */
}

// main.h

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "usart.h"
#include "gpio.h"
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */

上面出现了汉字字符,程序对英文字符的支持是最好的,汉字字符需要注意编码问题,KEIL MDK选择ARM Compiler V6编译器若要使汉字能正常显示,需要选择GB2312编码,但对于该编码AC6编译器会给出字符编码的警告信息,忽略即可。

在程序中使用了C语言的函数sprintf / memset / strlen,因此需要在头文件中包含相应的头文件,同时把main.c中包含的头文件转移到main.h中了。

到这里USART1 Interrupt工程就完成了,编译无错误(有几个跟汉字字符编码相关的警告可忽略),烧录到我们的STM32L475潘多拉开发板运行结果跟预期一致(注意需要选择波特率115200,勾选发送新行以便添加回车换行符):
USART1中断运行结果
该工程源码下载地址:https://github.com/StreamAI/STM32L4/tree/master/USART1_IT

2.3 C标准库 I / O 重定向

前面的工程代码使用sprintf进行字符串格式化,然后通过HAL_UART_Transmit_IT将格式化后的字符串发送给USART1,在进行C语言编程时,我们经常使用printf向串口打印数据,但上面的工程使用printf打印数据并没有在串口工具中显示出来,这是为什么呢?

我们常使用PC编写C语言程序,操作系统已经把C语言标准库的输入输出终端默认设置为键盘和屏幕了,所以我们可以直接使用printf / scanf向屏幕打印数据、从键盘获取数据。但在没有运行操作系统的单片机比如我们的STM32L475开发板中,printf / scanf并没有默认的显示/输入终端设备,我们如果想让printf / scanf向USART1发送、获取数据,需要通过代码指定C标准库输入/输出函数的控制终端设备。那么,如何指定C标准库的控制终端呢?

通过查询ARM文档(http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0475m/chr1358938931411.html)可知,重定义下面的函数即可:

#include <stdio.h>
namespace std {
  struct __FILE
  {
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
  };
  FILE __stdout;
  FILE __stdin;
  FILE __stderr;
  int fgetc(FILE *f)
  {
    /* Your implementation of fgetc(). */
    return 0;
  }
  int fputc(int c, FILE *stream)
  {
    /* Your implementation of fputc(). */
  }
  int ferror(FILE *stream)
  {
    /* Your implementation of ferror(). */
  }
  long int ftell(FILE *stream)
  {
    /* Your implementation of ftell(). */
  }
  int fclose(FILE *f)
  {
    /* Your implementation of fclose(). */
    return 0;
  }
  int fseek(FILE *f, long nPos, int nMode)
  {
    /* Your implementation of fseek(). */
    return 0;
  }
  int fflush(FILE *f)
  {
    /* Your implementation of fflush(). */    
    return 0;
  }
}

下面我们以实现printf打印数据到USART1为例(即重定义fputc函数)说明C标准库指定输入/输出控制终端的实现过程。重定义的fputc函数代码如下:

// Core\Src\usart.c

/* USER CODE BEGIN 0 */
//文件句柄重定向到标准库输入输出
struct __FILE
{
  int handle;
  /* Whatever you require here. If the only file you are using is */
  /* standard output using printf() for debugging, no file handling */
  /* is required. */
};

FILE __stdout;
FILE __stdin;
FILE __stderr;

//文件输出流重定向到串口USART1
int fputc(int ch, FILE *stream)
{
  /* Your implementation of fputc(). */
	HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,0xffff);
	while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET);		//等待发送结束
	return ch;
}
......

重定义fputc后就可以使用printf函数将数据打印到USART1输出了,我们使用printf函数简化前面工程中的代码,主要修改的地方如下:

// Core\Src\usart.c

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance == USART1)
    {
	    if(usart1_buf.Tx_end_flag == 0){
	       printf("\r\n\r\n发送完成,已发送字符个数为:%d\r\n",usart1_buf.TxSize);
			  
		   usart1_buf.TxSize = 0;
	       usart1_buf.Tx_end_flag = 1;
		   memset(usart1_buf.TxBuff,0,usart1_buf.TxSize);
	    }
	}
}

// main.c

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		if(usart1_buf.Rx_end_flag == 1){
			printf("\r\n\r\n接收到的字符个数为:%d\r\n接收到的数据为:\r\n",usart1_buf.RxSize);
			usart1_buf.TxSize = usart1_buf.RxSize;
			usart1_buf.Tx_end_flag = 0;

			if(HAL_UART_Transmit_IT(&huart1, usart1_buf.RxBuff, usart1_buf.RxSize) != HAL_OK){
				Error_Handler();
			}
      		colors = usart1_buf.RxSize;
			usart1_buf.RxSize = 0;
			usart1_buf.Rx_end_flag = 0;
			memset(usart1_buf.RxBuff,0,usart1_buf.RxSize);
		}else{
			
			if(times % 1000 == 0){
				printf("\r\n\r\n请输入数据,以回车键结束:\r\n");
			}
			
			RGB_ON((times/100)%2 ? colors : BLACK);
			
			times++;
			
			HAL_Delay(10);
		}		
  }
  /* USER CODE END 3 */

使用printf简化了不少代码,如果不需要发送完成回调函数,还可以简化更对代码,这里保留前面工程的控制逻辑。到这里代码修改完毕,编译无错误,烧录到开发板运行跟预期结果一致,工程代码下载地址:https://github.com/StreamAI/STM32L4/tree/master/USART1_IT_Printf

三、USART使用之DMA

上面中断发送/接收我们还是觉得效率不够高,特别是接收,为了获取一段完整的未知长度数据,我们只能进行逐个字符接收中断处理,浪费了很多CPU资源处理中断请求响应。那么,我们能否一次接收一段完整的数据(可以称为一帧数据)减少中断处理的频率呢?

从前面USART HAL API可以看出,USART是支持DMA(Direct Memory Access)的,DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM内存与I/O设备开辟一条直接传送数据的通道(对于USART1接收是直接将数据从USART1外设传输到内存中,对于USART1发送是直接将数据从内存中传输到USART1外设),能使CPU的效率大为提高,USART1的发送/接收效率自然也能提高不少。

3.1 DMA简介

DMA控制器(DMAC)是一种特殊的硬件,它用来管理数据传输和总线仲裁。当要发起数据传输时,它会发一个申请使用系统总线的DMA请求信号给CPU,CPU完成当前操作后,就会让出系统总线,同时会发一个DMA确认信号给DMA控制器。接下来,DMA控制器接管系统总线,开始数据传输。数据传输完毕后,DMA控制器会通知CPU重新接管总线,整个工作过程大概如下图所示:
DMA工作过程
我们开发板STM32L475中有两个DMA控制器,每个控制器有7个通道,每个通道能服务于特定的外设,下面首先看看STM32L475系统总线、DMA控制器、各个外设间的关系如下图所示:
DMA框图
两个DMA控制器各通道与相应外设引脚的映射关系如下图所示(限于篇幅,DMA1使用图示形式,DMA2使用表格形式):
DMA1各通道映射关系
DMA2各通道映射关系
要想使用DMA进行数据传输,自然少不了对DMA控制器进行配置,比如配置通道、外设地址、内存地址、传输数据量等,HAL库文件中DMA初始化结构体的定义如下:

// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_dma.h
/**
  * @brief  DMA Configuration Structure definition
  */
typedef struct
{
  uint32_t Request;                   /*!< Specifies the request selected for the specified channel.
                                           This parameter can be a value of @ref DMA_request */

  uint32_t Direction;                 /*!< Specifies if the data will be transferred from memory to peripheral,
                                           from memory to memory or from peripheral to memory.
                                           This parameter can be a value of @ref DMA_Data_transfer_direction */

  uint32_t PeriphInc;                 /*!< Specifies whether the Peripheral address register should be incremented or not.
                                           This parameter can be a value of @ref DMA_Peripheral_incremented_mode */

  uint32_t MemInc;                    /*!< Specifies whether the memory address register should be incremented or not.
                                           This parameter can be a value of @ref DMA_Memory_incremented_mode */

  uint32_t PeriphDataAlignment;       /*!< Specifies the Peripheral data width.
                                           This parameter can be a value of @ref DMA_Peripheral_data_size */

  uint32_t MemDataAlignment;          /*!< Specifies the Memory data width.
                                           This parameter can be a value of @ref DMA_Memory_data_size */

  uint32_t Mode;                      /*!< Specifies the operation mode of the DMAy Channelx.
                                           This parameter can be a value of @ref DMA_mode
                                           @note The circular buffer mode cannot be used if the memory-to-memory
                                                 data transfer is configured on the selected Channel */

  uint32_t Priority;                  /*!< Specifies the software priority for the DMAy Channelx.
                                           This parameter can be a value of @ref DMA_Priority_level */
} DMA_InitTypeDef;


/** @defgroup DMA_request DMA request
  */
#define DMA_REQUEST_0                     0U
......
#define DMA_REQUEST_7                     7U

/** @defgroup DMA_Data_transfer_direction DMA Data transfer direction
  */
#define DMA_PERIPH_TO_MEMORY         0x00000000U        /*!< Peripheral to memory direction */
#define DMA_MEMORY_TO_PERIPH         DMA_CCR_DIR        /*!< Memory to peripheral direction */
#define DMA_MEMORY_TO_MEMORY         DMA_CCR_MEM2MEM    /*!< Memory to memory direction     */

/** @defgroup DMA_Peripheral_incremented_mode DMA Peripheral incremented mode
  */
#define DMA_PINC_ENABLE              DMA_CCR_PINC  /*!< Peripheral increment mode Enable */
#define DMA_PINC_DISABLE             0x00000000U   /*!< Peripheral increment mode Disable */

/** @defgroup DMA_Memory_incremented_mode DMA Memory incremented mode
  */
#define DMA_MINC_ENABLE              DMA_CCR_MINC   /*!< Memory increment mode Enable  */
#define DMA_MINC_DISABLE             0x00000000U    /*!< Memory increment mode Disable */

/** @defgroup DMA_Peripheral_data_size DMA Peripheral data size
  */
#define DMA_PDATAALIGN_BYTE          0x00000000U       /*!< Peripheral data alignment : Byte     */
#define DMA_PDATAALIGN_HALFWORD      DMA_CCR_PSIZE_0   /*!< Peripheral data alignment : HalfWord */
#define DMA_PDATAALIGN_WORD          DMA_CCR_PSIZE_1   /*!< Peripheral data alignment : Word     */

/** @defgroup DMA_Memory_data_size DMA Memory data size
  */
#define DMA_MDATAALIGN_BYTE          0x00000000U       /*!< Memory data alignment : Byte     */
#define DMA_MDATAALIGN_HALFWORD      DMA_CCR_MSIZE_0   /*!< Memory data alignment : HalfWord */
#define DMA_MDATAALIGN_WORD          DMA_CCR_MSIZE_1   /*!< Memory data alignment : Word	*/    

/** @defgroup DMA_mode DMA mode
  */
#define DMA_NORMAL                   0x00000000U     /*!< Normal mode                  */
#define DMA_CIRCULAR                 DMA_CCR_CIRC    /*!< Circular mode                */

/** @defgroup DMA_Priority_level DMA Priority level
  */
#define DMA_PRIORITY_LOW             0x00000000U     /*!< Priority level : Low       */
#define DMA_PRIORITY_MEDIUM          DMA_CCR_PL_0    /*!< Priority level : Medium    */
#define DMA_PRIORITY_HIGH            DMA_CCR_PL_1    /*!< Priority level : High      */
#define DMA_PRIORITY_VERY_HIGH       DMA_CCR_PL      /*!< Priority level : Very_High */

DMA的初始化配置项也相对容易理解,就不再赘述了,下面看看DMA句柄及相应的HAL API部分代码:

// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_dma.h
/**
  * @brief  DMA handle Structure definition
  */
typedef struct __DMA_HandleTypeDef
{
  DMA_Channel_TypeDef    *Instance;                                                     /*!< Register base address                */

  DMA_InitTypeDef       Init;                                                           /*!< DMA communication parameters         */

  HAL_LockTypeDef       Lock;                                                           /*!< DMA locking object                   */

  __IO HAL_DMA_StateTypeDef  State;                                                     /*!< DMA transfer state                   */

  void                  *Parent;                                                        /*!< Parent object state                  */

  void                  (* XferCpltCallback)(struct __DMA_HandleTypeDef * hdma);        /*!< DMA transfer complete callback       */

  void                  (* XferHalfCpltCallback)(struct __DMA_HandleTypeDef * hdma);    /*!< DMA Half transfer complete callback  */

  void                  (* XferErrorCallback)(struct __DMA_HandleTypeDef * hdma);       /*!< DMA transfer error callback          */

  void                  (* XferAbortCallback)(struct __DMA_HandleTypeDef * hdma);       /*!< DMA transfer abort callback          */

  __IO uint32_t         ErrorCode;                                                      /*!< DMA Error code                       */

  DMA_TypeDef           *DmaBaseAddress;                                                /*!< DMA Channel Base Address             */

  uint32_t              ChannelIndex;                                                   /*!< DMA Channel Index                    */

}DMA_HandleTypeDef;


/** @addtogroup DMA_Exported_Functions_Group1 */
/* Initialization and de-initialization functions *****************************/
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_DeInit (DMA_HandleTypeDef *hdma);

/** @addtogroup DMA_Exported_Functions_Group2 */
/* IO operation functions *****************************************************/
HAL_StatusTypeDef HAL_DMA_Start (DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
HAL_StatusTypeDef HAL_DMA_Abort(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_Abort_IT(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_PollForTransfer(DMA_HandleTypeDef *hdma, HAL_DMA_LevelCompleteTypeDef CompleteLevel, uint32_t Timeout);
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_RegisterCallback(DMA_HandleTypeDef *hdma, HAL_DMA_CallbackIDTypeDef CallbackID, void (* pCallback)( DMA_HandleTypeDef * _hdma));
HAL_StatusTypeDef HAL_DMA_UnRegisterCallback(DMA_HandleTypeDef *hdma, HAL_DMA_CallbackIDTypeDef CallbackID);

/** @addtogroup DMA_Exported_Functions_Group3 */
/* Peripheral State and Error functions ***************************************/
HAL_DMA_StateTypeDef HAL_DMA_GetState(DMA_HandleTypeDef *hdma);
uint32_t             HAL_DMA_GetError(DMA_HandleTypeDef *hdma);

由于DMA一般由I/O外设请求请求,在DMA句柄中包含其父指针(*Parent),如果是USART1请求的DMA通道,则这里DMA父指针指向USART1句柄(USART1句柄也包含其TX/RX DMA通道的句柄)。DMA操作函数也可以按照前面介绍的HAL库函数类别进行划分,这里就不再赘述了,DMA操作函数常由其申请外设比如USART1的HAL操作函数调用。

3.2 CubeMX配置

以前面USART1中断工程为基础,新增DMA配置,从前面的DMA映射图表中可知,USART1 TX/RX既可以映射到DMA1也可映射到DMA2,这里以映射到DMA2 6/7通道为例创建工程。DMA配置大部分参数都按默认值即可,但USART1 RX的DMA通道模式这里改为了循环模式(Circular),若为正常模式(Normal)则待数据传输完毕DMA即停止传输,我们想USART1 RX随时有数据到来时DMA都能进行传输,所以需要这里配置为循环模式,其余配置均为默认值,DMA配置界面如下:
USART1 DMA2配置
接下来配置DMA通道的中断优先级,并使能中断,DMA的NVIC配置界面如下:
USART1 DMA NVIC配置
到这里USART1新增DMA配置便完成了,按照原先的设置直接生成工程即可。

3.3 完成用户控制逻辑

前面USART1中断接收完成处理程序逐个字符判断是否接收到结束标识符(’\r’ ‘ || '\n’),使用DMA该如何判断该段数据的结束标识呢?

我们回顾下DMA的寄存器,其中有一个通道x传输数据量寄存器DMA_CNDTRx,通过读取该寄存器可以获知该通道剩余待传输的字节数目,由于DMA缓冲区总字节数是已知的,我们便可以知道该通道已传输的字节数目,该寄存器信息及读取该寄存器值的宏定义如下(参考自《SMT32L475VE Reference manual.pdf》):
DMA CNDTRX信息

// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_dma.h
/**
  * @brief  Return the number of remaining data units in the current DMA Channel transfer.
  * @param  __HANDLE__ DMA handle
  * @retval The number of remaining data units in the current DMA Channel transfer.
  */
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR)

从上图也可以知道我们在前面讲USART1 RX DMA通道设置为循环模式的原因,同时也知道该寄存器在DMA通道使能时是只读的,若要修改则需要先停止/失能该DMA通道。通过读取USART1 RX DMA通道上该寄存器CNDTRx便可获知已接收数据的字节数,但我们何时读取该寄存器的值呢?也即我们如何判断该段数据已接收完毕了呢?

再回顾下前面介绍的USART1寄存器,其中有一个状态寄存器USART_SR,状态寄存器有一位IDLE可以监测到总线是否空闲(RXNE读数据寄存器非空标志位,如果接收到数据,则RXNE置位),如果总线空闲则IDLE置位,即RXNE被置位且接收数据已通过总线传输完毕时IDLE被置位,USART_SR中关于RXNE与IDLE的介绍及相关宏定义如下:
RXNE/IDLE介绍

// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_uart.h

 /** @brief  Check whether the specified UART flag is set or not.
  * @param  __HANDLE__ specifies the UART Handle.
  * @param  __FLAG__ specifies the flag to check.
  *        This parameter can be one of the following values:
  *            @arg @ref UART_FLAG_TXFT  TXFIFO threshold flag
  *            @arg @ref UART_FLAG_RXFT  RXFIFO threshold flag
  *            @arg @ref UART_FLAG_RXFF  RXFIFO Full flag
  *            @arg @ref UART_FLAG_TXFE  TXFIFO Empty flag
  *            @arg @ref UART_FLAG_REACK Receive enable acknowledge flag
  *            @arg @ref UART_FLAG_TEACK Transmit enable acknowledge flag
  *            @arg @ref UART_FLAG_WUF   Wake up from stop mode flag
  *            @arg @ref UART_FLAG_RWU   Receiver wake up flag (if the UART in mute mode)
  *            @arg @ref UART_FLAG_SBKF  Send Break flag
  *            @arg @ref UART_FLAG_CMF   Character match flag
  *            @arg @ref UART_FLAG_BUSY  Busy flag
  *            @arg @ref UART_FLAG_ABRF  Auto Baud rate detection flag
  *            @arg @ref UART_FLAG_ABRE  Auto Baud rate detection error flag
  *            @arg @ref UART_FLAG_CTS   CTS Change flag
  *            @arg @ref UART_FLAG_LBDF  LIN Break detection flag
  *            @arg @ref UART_FLAG_TXE   Transmit data register empty flag
  *            @arg @ref UART_FLAG_TXFNF UART TXFIFO not full flag
  *            @arg @ref UART_FLAG_TC    Transmission Complete flag
  *            @arg @ref UART_FLAG_RXNE  Receive data register not empty flag
  *            @arg @ref UART_FLAG_RXFNE UART RXFIFO not empty flag
  *            @arg @ref UART_FLAG_IDLE  Idle Line detection flag
  *            @arg @ref UART_FLAG_ORE   Overrun Error flag
  *            @arg @ref UART_FLAG_NE    Noise Error flag
  *            @arg @ref UART_FLAG_FE    Framing Error flag
  *            @arg @ref UART_FLAG_PE    Parity Error flag
  * @retval The new state of __FLAG__ (TRUE or FALSE).
  */
#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->ISR & (__FLAG__)) == (__FLAG__))

通过读取IDLE状态,待总线空闲时说明这一帧数据接收完毕,即IDLE置位时读取USART1 RX DMA通道上寄存器CNDTRx便可获知这一帧接收数据的字节数,按照这个思路编写USART1 RX DMA通道数据处理函数代码如下(HAL_UART_TxCpltCallback函数代码跟前面的工程一样,这里就略去了):

// Core\Src\usart.c

/* USART1 init function */

void MX_USART1_UART_Init(void)
{
	......
	 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);          //使能IDLE中断
}

/* USER CODE BEGIN 1 */
void HAL_UART_IDLECallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance == USART1){
		if(__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE) != RESET){		
			
			HAL_UART_DMAStop(huart);

			usart1_buf.Rx_end_flag = 1;
			__HAL_UART_CLEAR_IDLEFLAG(huart);//清除标志位
			usart1_buf.RxSize = USART1_RX_LEN - __HAL_DMA_GET_COUNTER(huart->hdmarx);

			HAL_UART_Receive_DMA(huart, usart1_buf.RxBuff, USART1_RX_LEN);
		}
	}
}
......
/* USER CODE END 1 */

// Core\Inc\usart.h

/* USER CODE BEGIN Prototypes */
void HAL_UART_IDLECallback(UART_HandleTypeDef *huart);
/* USER CODE END Prototypes */

// Core\Src\stm32l4xx_it.c

/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  HAL_UART_IDLECallback(&huart1);

  /* USER CODE END USART1_IRQn 1 */
}

要想使用IDLE标志位,首先需要使能UART_IT_IDLE中断,将这个使能IDLE中断的宏定义放到MX_USART1_UART_Init函数中了。监测IDLE中断发生后获知已接收到字节数的处理过程放到函数HAL_UART_IDLECallback中了,由于设置CNDTRx寄存器需要在DMA失能的情况下进行,因此在IDLE处理函数中先停止DMA最后再通过HAL_UART_Receive_DMA设置CNDTRx寄存器并使能DMA通道。由于HAL_UART_IDLECallback函数是我们自己定义的,所以需要在头文件中声明,同时需要在USART1中断处理函数USART1_IRQHandler中调用。

最后我们在main函数中实现控制逻辑,要实现的目的跟前面USART1中断工程类似,实现代码如下:

// Core\Src\main.c

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  uint16_t times = 0;
  uint16_t colors = BLACK;
  uint8_t *SendBuf = "\r\n请输入数据,以回车键结束:\r\n";
  /* USER CODE END 1 */
  ......
  /* USER CODE BEGIN 2 */

  if(HAL_UART_Receive_DMA(&huart1, usart1_buf.RxBuff, USART1_RX_LEN) != HAL_OK){
		Error_Handler();
	}
	
  if(HAL_UART_Transmit_DMA(&huart1, SendBuf, sizeof(SendBuf)) != HAL_OK){
		Error_Handler();
	}

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    if(usart1_buf.Rx_end_flag == 1 && usart1_buf.RxSize != 0){
			printf("\r\n\r\n接收到的字符个数为:%d\r\n接收到的数据为:\r\n",usart1_buf.RxSize);
			usart1_buf.TxSize = usart1_buf.RxSize;
			usart1_buf.Tx_end_flag = 0;

			if(HAL_UART_Transmit_DMA(&huart1, usart1_buf.RxBuff, usart1_buf.RxSize) != HAL_OK){
				Error_Handler();
			}
      		colors = usart1_buf.RxSize - 1;
			usart1_buf.RxSize = 0;
			usart1_buf.Rx_end_flag = 0;
			memset(usart1_buf.RxBuff,0,usart1_buf.RxSize);
		}else{
			
			if(times % 1000 == 0){
				printf("\r\n\r\n请输入数据,以回车键结束:\r\n");
			}
			
			RGB_ON((times/100)%2 ? colors : BLACK);
			
			times++;
			
			HAL_Delay(10);
		}
  }
  /* USER CODE END 3 */
}

在前面先调用HAL_UART_Receive_DMA与HAL_UART_Transmit_DMA是为了使能DMA通道传输,使用ARM Compiler V6编译无错误,烧录到开发板上运行跟预期一致,工程源码下载地址:https://github.com/StreamAI/STM32L4/tree/master/USART1_DMA

更多文章:

发布了65 篇原创文章 · 获赞 35 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/m0_37621078/article/details/100164277