HAL库STM32常用外设教程(三)—— USART/UART通信


一、USART/UART接口描述

  USART 表示 Universal Synchronous Asynchronous Receiver Transmitter,就是通过同步异步收发器,是一种串行通信接口。USART接口最多有5个信号。图1-1 是MCU上一个USART接口的5个信号及其输入/输出方向示意图。
在这里插入图片描述

图1-1 USART接口的5个信号
  • TX: 串行输出信号。
  • RX: 串行输出信号。
  • nCTS:允许发送(clear to send)信号,低电平有效,是发送给对方设备的一个信号。如果本机准备好接收数据,则将nRTS置为低电平,则表示对方设备准备好了接收数据,本机可以发送数据了;否则,不能发送数据。
  • nRTS:请求发送(request to send)信号,低电平有效,是发送给对方设备的一个信号。如果本机准备好了接收数据,则将nRTS置为低电平,通知对方设备可以发送数据了。
  • SCLK:发送器输出的时钟信号,这个时钟信号线仅用于同步信号。

  在这5个信号中,TX和RX是必需的。nCTS和nRTS称为硬件流控制信号,在异步通信时,可以选择是否使用硬件流控制,在同步通信时没有硬件流控制信号。SCLK只用于同步模式通信,异步通信时无SCLK信号。
  除了USART接口,还有一种UART(Universal Asynchronous Receiver Tramsmitter)接口,就是只有异步模式的串口。UART接口没有时钟信号SCLK,一般也没有硬件流控制信号nCTS和nRTS

二、开发板上的串口电路

2.1 串口之间的连接

  MCU上的串口是逻辑电平(TTL或CMOS电平),一些模块上的串口也是逻辑电平,如WiFi模块、蓝牙模块、GPS模块等,MCU和这些模块之间可以通过串口信号线直接连接的。
  串口一般工作于异步模式,可以使用硬件流控制信号提高通信的准确性。当两个设备通过逻辑电平串口直接连接时,两个数据线要交叉连接,硬件流控制信号线也需要交叉连接,如图2-1所示。

在这里插入图片描述

图2-1 USART接口的5个信号

补充:
  TTL(Transistor-Transistor Logic)电平和CMOS(Complementary Metal-Oxide-Semiconductor)电平是数字电路中常见的两种逻辑电平标准:
(1)TTL电平:
① TTL电平指的是基于晶体管-晶体管逻辑的数字电路标准。
逻辑低电平0 一般被定义为0V至0.8V之间,逻辑高电平1 一般被定义为2V至5V之间
③ TTL电平特点包括速度较快、功耗较高、噪声容限低等。
(2)CMOS电平:
① CMOS电平是基于互补金属氧化物半导体技术的数字电路标准。
逻辑低电平0 一般被定义为0V至0.3V之间,逻辑高电平1 一般被定义为0.7V至供电电压之间
③ CMOS电平特点包括功耗低、噪声容限高、集成度高等。


2.2 串口与RS232的转换与连接(了解)

  有的台式计算机主机后面有RS232接口,还有一些嵌入式设备上也有RS232接口,是一种9针的D形口。RS232也是串口,与UART使用相同的底层通信协议,只是物理层的信号电平不同。在RS232接口中,用 - 15 ~ -3V表示逻辑1,用+3 ~ +15V表示逻辑0。MCU可以和具有RS232接口设备之间进行串口通信,但是需要进行RS232电平与逻辑电平之间的转换。
  开发板上有一个SP232芯片用于进行逻辑电平和RS232电平之间的转换,电路如图2-1所示。开发板上还有两个RS232电平的DB9接口,就是图2-1中的USART_F(母头)和USART_M(公头)。SP232具有两路转换功能,一路是串口信号U2_TX/U2_RX和COM2母头RS232之间额转换,另一路是串口信号U3_TX/U3_RX和COM3公头之间的转换。
在这里插入图片描述

图2-1 开发板上的MCU串口与RS232之间的电平转换电路*

  跳线座P9和P10用于设置串口信号U2_TX/U2_RX和U3_TX/U3_RX的来源。信号U2_TX/U2_RX可以来源于STM32F407的USART2,U3_TX/U3_RX可以来源于STM32F407的USART3。通过跳线座的P9和P10,可以将STM32F407的USART3/USART2输出到DB9公头或DB9母头上。

  • 将跳线座P9的1与3短接、2与4短接,就是将STM32f407的USART2输出到DB9母头。
  • 将跳线座P10的1与3短接、2与4短接,就是将STM32f407的USART3输出到DB9公头。

  如果使用9针交叉串口数据线,就可以在两个STM32F407开发板之间进行RS232双机串口通信。

2.3 串口与RS485的转换与连接(了解)

  RS485是另一种串行通信电气标准,它采用两根信号线上的差分电压表示不同的逻辑信号。RS485接口的设备可以组成RS485网络,网络中有一个模块和多个从设备,RS485网络通信距离可达1200m,所以适合用于工业现场。一些工业测控模块使用RS485接口。
  MCU若要通过串口与RS485网络上的设备通信,必须进行逻辑电平与RS485电平之间的转换。开发板上就有一个MCU和RS485网络的转换电路,如图2-2和图2-1所示。这个电路用到了STM32F407的USART2接口,RS485网络上只有两根信号线,即A_M和B_M,芯片SP3485实现串口的逻辑电平与RS485电平之间的转换。
在这里插入图片描述

图2-2 串口与RS485的转换电路

注:
RS232和RS485的区别
RS232 :
(1)通常使用单端口(全双工)。
(2)传输电平信号接口的信号电平值较高(信号“1”为“-3V至-15V”,信号“0”为“3至15V”),与RS485相比,RS232使用单端信号传输方式,即每个信号都是相对于公共参考地(GND)传输的,而不是采用差分信号。
(3)适合在段距离内(几十米)进行数据传输。
(4)常用于单点连接,即一个发送器和一个接收器的连接方式。

RS485 :
(1) 多端口查分传输(半双工)。
(2)传输差分信号逻辑“1”以两线间的电压差为+(2—6) V表示。
(3)逻辑“0”以两线间的电压差为-(2—6)V表示,适合长距离(几千米)数据传输。
(4)可以支持多点连接,即多个发送器和接收器之间的连接,常用于工业控制系统中多个设备之间的通信

2.4 串口与USB的转换与连接(了解)

  图2-3和图2-4所示的数据线可以进行USB到RS232之间的转换,还有一些芯片可以实现USB到逻辑电平串口的转换,常用的此类芯片有CH340、PL2303等。开发板上就有一个CH340芯片构成的电路,电路如图所示。
  注意,使用这样的USB线连接计算机与开发板之后,还需要安装开发板光盘上提供的USB转串口CH342的驱动程序
  开发板还可以从这个MicroUSB接口获取5V电源为开发板供电。所以,如果没有独立的5VDC电源为开发板供电时,可以使用这个MicroUSB接口为开发板供电,通过这个MicroUSB接口连接计算机后,在计算机上还可以使用开发板厂家提供的专用软件为MCU下载编译后的hex二进制程序文件。
  图2-3和图2-4中CH340转换出的串口信号可以连接STM32F407的USART1。跳线P6的1和2、3和4短接时连接STM32F407的USART1,这也是默认连接。

在这里插入图片描述

图2-3 使用CH340G的 USB转串口电路(1)

在这里插入图片描述

图2-4 使用CH340的USB转串口电路(2)

三、串口通信参数

3.1 通信方式

  从硬件上看,串行通信方式有单工、半双工、全双工通信。
 (1)单工通信。数据只允许向一个方向进行传输,即数据发送设备只能发送数据,数据接收设备只能接收数据。此时在数据发送设备和数据接收设备之间之需要一条数据传输线。
 (2)半双工通信。数据允许向两个方向进行传送,当时传送的过程与接收数据过程不能同时进行。即进行通信的两个设备都具备发送和接收数据的能力。但是同一时刻只能有一个设备进行数据发送而另一个设备进行数据接收。
 (3)全双工通信。数据允许两个方向进行传输,并且发送数据的过程和接收数据的过程可以同时进行。即进行通信的两个设备都具备发送和接收数据的能力,而且同一时刻两个设备均可以发送和接收数据。

3.2 串口通信数据传输形式

  串口硬件层的功能就是进行串口数据发送和接收,发送和接收的基本单元室一个数据帧,传输一个8位字长的数据帧的时序图如图3-1所示。

在这里插入图片描述
串口通信的基本参数有以下几个。

  • 数据帧:8位或9位,一般设置为8位,因为1字节是8位,这样一帧传输1字节的有效数据。
  • 奇偶校验位:可以无奇偶校验位,也可以设置奇偶校验或偶校验位。
  • 停止位:1个或2个停止位,一般设置为1个停止位。
  • 波特率:就是串行数据传输的速率,常用的波特率有9600、19200、115200等。一个串口单元的时钟由APB1或APB2总线提供,所以挂在不同APB总线上的串口单元的最高布特率不同。
      另外,STM32F4的串口还有一个过采样(over sampling)参数,可以设置为8次采样或16次采样。过采样用于确定有效的起始位。8次采样速度快,但是容错性差,16次采样速度慢,但容错性好,默认使用16次采样。

四、串口的HAL驱动程序

4.1 常用的功能函数

  串口的驱动程序头文件是stm32f4xx_hal_uart.h。串口操作的常用HAL库函数如表1-1所示。

表1-1 串口操作的HAL函数
分组函数名功能说明
初始化和总体功能HAL_UART_Init()串口初始化,设置串口通信·参数
初始化和总体功能HAL_UART_MspDeInit()串口初始化的2MSP弱函数,在HAL_UART_Init()中被调用。重新实现的这个函数一般用于串口引脚的GPIO初始化和中断设置
初始化和总体功能HAL_UART_GetState()获取串口当前状态
初始化和总体功能HAL_UART_GetError()返回串口错误代码
阻塞式传输HAL_UART_Transmit()阻塞式发送一个缓冲区的数据,发送完成或超时后才返回
阻塞式传输HAL_UART_Receive()阻塞方式将数据接收到一个缓冲区,接收完成或超时后返回
中断方式传输HAL_UART_Transmit_IT()以中断方式(非阻塞)发送一个缓冲区的数据
中断方式传输HAL_UART_Receive_IT以中断方式(非阻塞)将指定长度的数据接收到缓冲区
DMA方式传输HAL_UART_Transmit_DMA()以DMA方式发送一个缓冲区的数据
DMA方式传输HAL_UART_Receive_DMA()以DMA方式将指定长度的数据接收到缓冲区
DMA方式传输HAL_UART_DMAPause()暂停DMA传输过程
DMA方式传输HAL_UART_DMAResume()继续先前暂停的DMA传输
DMA方式传输HAL_UART_DMAStop()停止DMA传输过程
取消数据传输HAL_UART_Abort()终止以中断方式或DMA方式启动的传输过程,函数自身以阻塞方式运行
取消数据传输HAL_UART_AbortTransmit()终止以中断方式或DMA方式的数据发送过程,函数自身以阻塞方式运行
取消数据传输HAL_UART_AbortReceive()终止以中断方式或DMA方式的数据接收过程,函数自身以阻塞方式运行
取消数据传输HAL_UART_Abort_IT()终止以中断方式或DMA方式启动的传输过程,函数以非阻塞方式进行
取消数据传输HAL_UART_AbortTransmit_IT()终止以中断方式或DMA方式的数据发送过程,函数自身以非阻塞方式运行
取消数据传输HAL_UART_AbortReceive_IT()终止以中断方式或DMA方式的数据接收过程,函数自身以非阻塞方式运行

这里主要讲解一下串口的初始化函数:
  函数HAL_UART_Init() 用于串口的初始化,主要设置串口通信参数。其函数定义如下:

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);

  参数 huartUART_HandleTypeDef 类型的函数指针,是串口外设对象指针。在CubeMx生成的串口程序文件usart.c里,会有一个串口定义外设对象变量,如:

UART_HandleTypeDef huart1;    /* USART1的外设对象变量 */

  结构体类型 UART_HandleTypeDef 的定义如下,各成员变量见注释:

typedef struct __UART_HandleTypeDef
{
  USART_TypeDef                 *Instance;        /* UART寄存器基址 */
  UART_InitTypeDef              Init;             /* UART通信参数 */
  const uint8_t                 *pTxBuffPtr;      /* 发送数据缓冲区指针*/
  uint16_t                      TxXferSize;       /* 需要发送数据的字节数 */
  __IO uint16_t                 TxXferCount;      /* 发送数据计数器,递增计数 */
  uint8_t                       *pRxBuffPtr;      /* 接收数据缓冲区指针 */
  uint16_t                      RxXferSize;       /* 需要接收数据的字节数 */
  __IO uint16_t                 RxXferCount;      /* 接收数据计数器,递减计数 */
  __IO HAL_UART_RxTypeTypeDef ReceptionType;      /* 标识当前接收到的数据类型 */
  DMA_HandleTypeDef             *hdmatx;          /* 数据发送DAM流对象指针  */
  DMA_HandleTypeDef             *hdmarx;          /* 数据发送DMA流对象指针 *
  HAL_LockTypeDef               Lock;             /* 锁定类型 */
  __IO HAL_UART_StateTypeDef    gState;           /* UART状态 */
  __IO HAL_UART_StateTypeDef    RxState;          /* 发送操作相关到的状态 */
  __IO uint32_t                 ErrorCode;        /* 错误码  */
} UART_HandleTypeDef;

  结构体 UART_HandleTypeDef 的成员变量 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;              /* 过采样 */
} UART_InitTypeDef;

4.2 阻塞式数据传输

串口数据传输有两种模式:阻塞模式和非阻塞模式。
(1)阻塞模式(block mode)就是轮询模式,例如,使用函数HAL_UART_Transmit()发送一个缓冲区的数据时,这个函数会一直执行,直到数据传输完成或超时以后,函数才返回。
  以阻塞模式发送数据的函数是HAL_UART_Transmit(),其原型定义如下:

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

  其中,参数pData 是缓冲区指针;参数 Size 是需要发送的数据长度(字节);参数Timeout 是超时,用滴答信号的节拍数表示。函数HAL_UART_Transmit()以阻塞模式发送一个缓冲区的数据,若返回为HAL_OK,表示传输成功,否则可能是超时或其他错误。参数Timeout的单位嘀嗒信号的节拍数,当Sytick定时器的定时周期是1ms时,Timeout 的单位就是ms。
  以阻塞模式接受数据的函数是HAL_UART_Receive(),其原型定义如下:

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

  其中,参数 pData 是用于存放接收数据的缓冲区指针;参数 Size 是需要接收的数据长度(字节);Timeout 是超时限制时间,单位是嘀嗒信号的节拍数,默认情况下就是ms。
  函数HAL_UART_Receive() 以阻塞模式将指定长度的数据接收到缓冲区,若返回值为HAL_OK,表示接收成功,否则可能是从超时或其他错误。

4.3 非阻塞式数据传输

  非阻塞模式(non-blocking mode)是使用中断或DMA方式进行数据传输,例如,使用函数 HAL_UART_Transmit_IT() 启动一个缓冲区的数据传输后,该函数立即返回。数据传输的过程引发各种事件中断,用户在相应的回调函数里进行处理。

4.3.1 非阻塞式 —— 中断方式

  以中断方式发送数据的函数是 HAL_UART_Transmit_IT(),其原型定义如下:

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

  其中,参数 pData 是需要发送的缓冲区指针,参数Size是需要发送的数据长度(字节)。这个函数以中断的方式发送一定长度的数据。若函数返回值为HAL_OK,表示启动成功,但并不表示已经接收完成。数据发送结束时,会触发中断并调用中断回调函数HAL_UART_TxCpltCallback(),若要在数据发送结束时做一些处理,就需要重新实现这个回调函数。
  以中断方式接收数据的函数是HAL_UART_Receive_IT(),其原型定义如下:

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

  其中,pData是存放接收数据的缓冲区的指针,参数Size是需要发送的数据长度(字节)。这个函数以中断方式接收一定长度的数据,如函数返回值为HAL_OK,表示启动成功,但并不表示已经接完数据了。数据接收完成后,会触发中断并调用回调函数HAL_UART_RxCpltCallback(),若要在接收完数据后做一些处理,就需要调用这个回调函数。
  函数HAL_UART_Receive_IT()有一些特性需要注意。
① 这个函数执行一次只能接收固定长度的数据,什么时候进入中断是根据Size来判断的,如果Size设置的数值为1字节,那么没接收一个字节就会产生于一个中断,接收1000个字节就会中断1000次,需要注意的是,频繁的中断就意味着会打断其他代码的执行,对一些应用场景是不合适的,此时可以考虑接下来介绍的DMA+串口的组合。
② 在完成数据接收后会自动关闭接收中断,不会在继续接收数据,即这个函数是“一次性”的。若要再接收下一批数据,需要再次执行这个函数。函数HAL_UART_Receive_IT()的这些特性,使其在处理不确定长度、不确定输入时间的串口数据时比较麻烦。

4.3.2 非阻塞式 —— DMA方式

  以DMA方式发送数据的函数是HAL_UART_Transmit_DMA(),其原型定义如下:

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

  其中,参数pData是需要发送的缓冲区指针,参数Size是需要发送的数据长度(字节)。这个函数以DMA的方式发送一定长度的数据。若返回值为HAL_OK,表示DMA传输启动成功,但并不表示已经发送完成。数据发送结束时,会触发DMA传输完成中断并调用中断回调函数HAL_UART_TxCpltCallback(),若要在数据发送结束时做一些处理,就需要重新实现这个回调函数。

  以DMA方式接收数据的函数是HAL_UART_Receive_DMA(),其原型定义如下:

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

  其中,pData是存放接收数据的缓冲区的指针,参数Size是需要接收的数据长度(字节)。这个函数以DMA方式接收一定长度的数据,如函数返回值为HAL_OK,表示DMA传输启动成功,但并不表示已经接完数据了。数据接收完成后,会触发DMA传输完成中断并调用回调函数HAL_UART_RxCpltCallback(),若要在接收完数据后做一些处理,就需要重新实现这个回调函数。
  函数HAL_UART_Receive_DMA()
  相比中断方式,DMA方式传输数据在速度和效率上有更大的优势,适用于大数据量和高频率的数据传输。

4.4 中断事件与回调函数

   一个串口只有一个中断号,也就是ISR,例如,USART1的全局中断对应的ISR的USART1_IRQHandler()。在CubeMx自动生成代码时,其ISR框架会在文件stm32f4_it.c中生成,代码如下:

void USART2_IRQHandler(void)
{
   HAL_UART_IRQHandler(&huart2);
}

  所有串口的ISR都是调用HAL_UART_IRQHandler()这个处理函数,这个函数是中断处理通用函数。这个函数会判断产生的中断事件类型、清除事件中断标号、调用中断事件对应的回调函数。
  常用的回调函数有HAL_UART_TxCpltCallback()HAL_UART_RxCpltCallback()。在以中断或DMA方式发送数据完成时,会触发UART_IT_TC事件中断,执行回调函数HAL_UART_TxCpltCallback();在以中断或DMA方式接收数据完成是,会触发UART_IT_RXNE事件中断,执行回调函数HAL_UART_RxCpltCallback()

五、串口通信示例

  示例内容:通过串口助手向单片机发送“123456789”,单片机接收到数据后将“123456789”再发送出来。

5.1 串口 阻塞方式接收数据

5.1.1 原理概述

  单片机的串口阻塞式接收模式是指在接收数据时,程序会暂停执行直到接收到所需的数据为止。在这种模式下,接收函数会等待数据到达并阻塞主程序的执行,直到串口接收缓冲区中有数据可供读取。

5.1.2 STM32CubeMX配置

  在本次示例中用到的串口是USART1,下面是针对于STM32CubeMX里面的USART的配置进行解释:
在这里插入图片描述

图5-1 串口CubeMx配置

  模式配置的参数只有以下两个:
(1)Mode: 工程模式,设置为Asynchronous(异步),也就是串口最常用的模式。还有其他一些工作模式,如Synchronous(同步)、IrDA(红外通信)、Smartcard(智能卡)等。
(2)Hardware Flow Control(RS232): 硬件流控制。对于本示例,图5-1中的USART1接口并没有使用硬件流信号,所以设置为Disable。其他选项有CTS Only、RTS Only、CTS/RTS。如果使用硬件流控制,一般是CTS和RTS同时使用。注意,只有异步模式才会有硬件流控制信号。
  参数设置部分包括串口通信的4个基本参数和STM32的两个扩展参数。4个基本参数如下:
(1)Baud Rate波特率。这里设置为57600bit/s,波特率由串口所在的APB总线频率、过采样设置、波特率寄存器USART_BRR的设定值决定。在CubeMX中,只要直接设置波特率,CubeMx会根据设置波特率自动设置相关寄存器的内容。在图5-1中,单击参数设置界面右上角的图标,可以显示参数的简单信息。
(2)Word Lendth字长(包括奇偶校验位)。可选8位或9位,这里设置为8位。
(3)Parity:奇偶校验位。可选None(无)、Even(偶校验)和Odd(奇校验)。这里设置为None。如果设置奇偶校验位,字长应该设置为9位。
(4)Stop Bits停止位。可选1或2位,这里设置为1位。

  STM32扩展的两个参数如下:
(1)Data Direction: 数据方向。这里设置为Receive and Transmit(接收和发送),还可以设置只接收或只发送。
(2)Over Sampling过采样。可选16 Smaples 或8 Smaples,这里设置为16 Smaples。选择不同的过采样数据会影响波特率的可设置范围,而CubeMX会自动更新波特率的可设置范围。

注:
(1)过采样率的概念:
  UART通信中,过采样率是指在一个比特时间内采样的次数。常见的过采样率有8倍(8x)和16倍(16x)。例如,对于16x过采样率,UART在一个比特周期内采样16次,以决定该比特是1还是0。
(2)波特率可设置范围的变化:
  16倍过采样时,需要更高的时钟频率才能生成同样的波特率,这使得高波特率的生成更加困难。因此,支持的波特率范围可能较窄。
  8倍过采样时,由于分频更少,可以支持更高的波特率,但会影响波特率的精度和容错能力。因此,支持的波特率范围可能更宽,但通信稳定性可能受影响。

  这样设置USATRT后,CubeMX会自动配置PA9和PA10作为USATR1_TX和USART1_RX信号复用引脚,这与电路上是一致的,也无需再做任何GPIO设置。USART1的GPIO引脚字动配置的结果如图5-2所示:

在这里插入图片描述

图5-2 串口CubeMx引脚配置

5.1.3 程序设置

  STM32CubeMx配置结束后,生成工程。先进行初步分析STM32CubeMx生成的工程,以便更好的理解STM32CubeMx里面的配置。

在这里插入图片描述

  • huart1.Instance = USART1;
      设置了huart1结构体的Instance成员为USART1,表示使用的是USART1串口。
  • huart1.Init.BaudRate = 115200;
      设置串口的波特率为115200。波特率是串口通信中传输速率的单位,表示每秒钟传输的比特数。
  • huart1.Init.WordLength = UART_WORDLENGTH_8B;
      设置数据帧的数据位长度为8位。这表示每个数据帧中有8个位用于传输数据。
  • huart1.Init.StopBits = UART_STOPBITS_1;
       设置停止位为1位。停止位用于指示数据帧的结束。
  • huart1.Init.Parity = UART_PARITY_NONE;
      设置校验位为无校验位。校验位用于检测数据传输中是否出现错误。
  • huart1.Init.Mode = UART_MODE_TX_RX;
      设置串口工作模式为发送和接收模式。即该串口既可以发送数据,也可以接收数据。
  • huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
      设置硬件流控制为无流控制。硬件流控制可以通过硬件信号线来控制数据流的流向,这里禁用了硬件流控制。
  • huart1.Init.HwFlowCtl = huart1.Init.OverSampling = UART_OVERSAMPLING_16;
      设置过采样率为16。过采样率影响串口接收的性能,一般设置为16以提高系统的容错性和抗噪声能力。
  • if (HAL_UART_Init(&huart1) != HAL_OK);
      调用HAL库提供的UART初始化函数HAL_UART_Init() 对huart1结构体所描述的USART1串口进行初始化。如果初始化失败(返回值不等于HAL_OK),则调用Error_Handler()函数处理错误情况。

  了解了上面的usart.c 里面的基础配置后,开始在生成的工程上编写用户代码 实现串口的阻塞接收功能。
  阻塞式接收相对于中断和DMA方式相对简单,按照上面CubeMx的步骤配置完成后,只需要在While(1)循环里面加入下面的代码就可以进行测试。

  if(HAL_UART_Receive(&huart1,&data,1,100) == HAL_OK){	 /* 接收的数据放入data	*/
	  HAL_UART_Transmit(&huart1,&data,1,100);		     /* 将data的数据发送出去	*/
   }

在这里插入图片描述

5.1.4 示例结果

  打开串口(友善串口调试助手,安装包放在文章末尾百度网盘内),在发送栏输入发送内容同,发送后会接受到一条同样的内容,具体示例如下:
在这里插入图片描述
  这种方式确实可以实现串口的接收和发送,不过也仅仅是实现,因为在实际中应用时一般不会在接收数据时就不管其它的响应了,只去做接收数据这一件事。所以这种方式的弊端还是比较大的,怎么解决这个问题呢,如果接收数据的时候还能做其他的响应就好了 ,如果有这种疑问,那就可以尝试一次串口的另一种方式——中断方式接收数据。

5.2 串口 中断方式接收数据

5.2.1 原理概述

   中断式串口接收机制基于STM32的硬件中断特性,当串口接收到数据时自动触发CPU中断,即时处理接收数据,无需CPU主动轮询,提高了CPU的处理效率。

5.2.2 STM32CubeMx配置

   串口接收中断的CubeMx配置是在阻塞式串口CubeMx配置的基础上进行如下操作,在“NVIC Settings”中开启串口1的全局中断。
在这里插入图片描述
补充:
   CubeMx里面的“NVIC Settings”选项功能:
(1)中断使能:
   在"NVIC Settings"中,你可以选择使能或禁用某些外部中断或定时器中断。这决定了这些中断源是否能够引发中断服务例程(ISR)。
(2)中断优先级配置:
   STM32的NVIC允许你为每个中断源分配优先级。优先级决定了在多个中断同时发生时,哪个中断应该被优先处理。优先级分为主优先级和子优先级,具体取决于你的MCU的配置。
优先级配置通常是通过两个值:Preemption Priority 和 Sub Priority 来实现的。主优先级控制中断的整体优先顺序,子优先级则在同一主优先级内的中断之间决定处理顺序。
(3)中断优先级分组:
  STM32允许你设置优先级分组(Priority Group),即分配优先级的位数如何在主优先级和子优先级之间分配。这决定了中断的优先级和响应行为的灵活性。

5.2.3 程序设置

   通过中断接收数据的方式整体思路如下,先开启串口接收中断,当检测到串口接收到数据时会触发串口接收中断回调函数,然后在回调函数里面进行处理,但是,实际应用中应该遵循中断中快进快出的原理,在中断回调函数里面进行处理时会占用中断太多的时间,不合理,所以在本次示例中是通过在回调函数中设置一个标志,在主循环中通过判断这个标志来处理数据的,即 在回调函数中将标志置位表示已经接收到数据,然后主循环查到这个标志 置位后就开始处理接收到的数据,处理完后再将标志清除,这样表示一次接收中断 处理数据的完成。具体代码如下:

  在配置好CubeMx后生成工程,然后开始下面的程序编写:
(1)先定义用到的变量,因为需要接受和发送和内容是“123456789”,所以定义了一个有9个元素的数组,每一个元素的数据类型是无符号8位整型,另一个是接收数据用到的标志。
在这里插入图片描述

uint8_t data[9] = {0};  	 /* 接收串口数据的数组 */
uint8_t clear_flag = 0;	 	 /* 接收到数据标志 */

(2)
在这里插入图片描述

①先在初始化位置开启串口中断接收:

  HAL_UART_Receive_IT(&huart1, (uint8_t *)data,9); 	/* 开启串口中断接收数据  */ 

②在主循环里面添加接收到数据后的处理,首先将数据发送出来,在完成数据接收后会自动关闭接收中断,不会在继续接收数据,所以需要通过HAL_UART_Receive_IT函数开启下一次的接收,最后将本次接收标志清0。

		if(clear_flag){																				/* 判断是否接收到数据  */ 
			HAL_UART_Transmit_IT(&huart1, (uint8_t *)data, 9);/* 将接收到的数据再发送出去 */ 
			HAL_UART_Receive_IT(&huart1, (uint8_t *)data, 9); /* 开启串口中断接收数据  */ 
			clear_flag = 0;																		/* 将接收数据标志清0,等待下一次接收数据  */ 
		}

  本示例中,HAL_UART_Receive_IT ( UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size )
函数的最后一个参数设置的9,表示接收9个字节数据后就会调用中断回调函数HAL_UART_RxCpltCallback,在该回调函数里面将接收标志进行置位,中断里面遵循快进快出的原则,所以将处理的过程都放在了中断外边(本次示例中放到了主循环中)。
  需要注意的是,虽然接收到 “size” 个字节才调用中断回调函数进行处理,但是在接收数据的过程中会不断地触发中断(不会阻塞)。可以进入Debug状态 在"stm32f4xx_it.h"文件中的串口中断服务函数“ HAL_UART_IRQHandler(&huart1);”处打个断点进行观察,以此判断是不是总进中断。

(3)回调函数设置
在这里插入图片描述

/* 串口接收中断回调函数  */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1){   		/* 判断是不是USART1接收到的数据  */
     clear_flag = 1;										/* 通过标志放到外边进行处理,不在中断中处理,中断中快进快出  */
  }
}

5.2.4 示例结果

  打开串口(友善串口调试助手,安装包放在文章末尾百度网盘内),在发送栏输入发送内容,发送后会接受到一条同样的内容,具体示例如下:
在这里插入图片描述

  在上面提到了一个问题,用中断的方式确实可以避免阻塞的情况,CPU在接收数据的时候可以去做其他的事情,但是接收的数据少还好,一旦接收的数据太多,CPU就会频繁的进入中断,这样CPU的利用率就会降低,那怎样解决这个问题呢,如果接收数据时能够不阻塞,并且不用频繁的进入中断那就好了,如果有这个疑问,可以尝试下下面的这种串口接收的方式——串口DAM接收。

5.3 串口 DMA方式接收数据

5.3.1 原理概述

  串口DMA的原理是通过DMA控制器直接在内存和串口之间传输数据,而无需CPU干预。DMA控制器负责自动完成数据的读取和写入,从内存的缓冲区到串口的数据寄存器(发送)或从串口的数据寄存器到内存(接收)。这样可以显著提高数据传输效率,减轻CPU的负担。完成数据传输后,DMA控制器会发出中断信号通知CPU处理后续任务。

5.3.2 STM32CubeMX配置

   串口接收中断的CubeMx配置是在中断式串口CubeMx配置的基础上进行如下操作,在“DMA Settings”中进行配置。
在这里插入图片描述

在这里插入图片描述

   选择“DMA Setting”配置DMA流对象。一个DMA流对象包含一个DMA请求和一个DMA流,以及DMA传输属性的各种配置参数。DMA数据传输是单向的,USART1有USART1_RX和USART1_TX的两个DMA请求,需要分别配置DAM流对象,表格下面的Add和Delete按钮可用于添加和删除DMA流对象。表中的每个DMA流对象有4列参数需要配置。

  • DMA Request :外设或存储器的DMA请求,也就是通道。USART1和USART1_RX(接收)和USART1_TX两个DMA请求。

  • Stream:DMA流。每个DMA请求可以用的DMA流会自动列出,选择一个即可。

  • Direction:传输方向。也就是DMA传输模式,会根据USART1接收的数据存入缓存区,所以方向是Peipheral To Memory(外设到存储器);USART1_TX和USART1的DMA数据输出请求,将缓冲区的数据用USART1输出,所以方向是 Memory To Peipheral(存储器到外设)。

  • Priority:优先级别。DMA流的软件优先级有Low、Medium、High、Vrey High这4个选项。
    在表格中选择一个DMA流对象后,在下面的面板上还可以设置DMA传输的更多参数。主要参数包括以下几项。
    在表格中选择一个DMA流对象后,在下方的面板上还可以设置DMA传输的更多参数。
    主要的参数包含以下几项。

  • Mode:DMA工作模式。可选Normal或Circular。USART1_RX的DMA工作模式选择为Circular,这样串口就可以自动重复接收数据;USART1_TX的DMA工作模式选择为Normal,发送一个缓冲区的数据后就停止。
    补充:
       (1)正常(Normal)模式是指传输一个缓冲区的数据后(传输数据量的大小就是一个DAM传输数据缓冲区的大小),DMA传输就停止了,若需要再传输一次缓冲区的数据,就需要再启动一次DMA传输。例如,在正常模式下,执行函数HAL_UART_Receive_DMA()接收固定长度的数据,接收完成后就不再继续了,这与中断方式接收函数HAL_UART_Receive_IT()类似。
      (2) 循环(Circular)模式是指启动一个缓冲区的数据传输后,会循环执行这个DMA数据传输任务,例如,在循环模式下,只需要执行一次HAL_UART_Receive_DMA(),就可以连续重复地进行串口数据的DMA接收,接受满一个缓冲区的数据后,产生的DMA传输完成事件中断。这可以很好的解决串口输入连续监测的问题。

  • Use Fifo:是否使用FIFO。如果使用FIFO,还需要设置FIFO阀值(Threshold)。在使用FIFO时还可以使用突发传输,需要设置突发传输的增量节拍数。本示例不使用FIFO。

  • Data Width:数据宽度。外设和存储器需要单独设置数据宽度,数据宽度有Byte、Half Word和Word。串口传输数据的数据的基本单位是字节,缓冲区的基本单位也是字节。

  • Increment Address:地址自增。这里指DMA传输一个基本数据单位后,外设或存储器的地址是否自动增加,地址增加的大小等于数据宽度。例如上图中USART1_RX的DMA设置,串口的地址是固定的,用于存储接收数据的缓冲区在没接收1字节后,存储器的地址指针应该自动移动1字节。所以,Memory使用地址自增,而Peripheral不使用地址自增。
       为DMA请求配置DMA流之后,用到的DMA流的中断会自动打开,要对DMA流的中断进行响应和处理,就必须开启USART1的全局中断。在NVIC组件里设置中断的优先级,如下图所示。

在这里插入图片描述

5.3.3 程序设置

   通过DMA接收数据思路和通过中断接收·数据的程序实现思路是十分相似的。先启动DMA接收,当接收到一定量的数据后触发串口中断回调,在回调里面置标志,在主循环里面判断标志进行处理函数,具体代码如下:
(1)先定义串口接收的数组和标志,因为知道了发送的数据是“123456789”,所以定义了具有9个元素的数组,每个元素都是无符号8位整型。
在这里插入图片描述

uint8_t RxBuffer[9] = {0};  	 /* 接收串口数据的数组 */
uint8_t clear_flag_DMA = 0;	 	 /* 接收到数据标志 */

(2)接收与处理设置
①首先在初始化后面打开DMA接收,开启第一次的DMA接收。
②在主循环里进行判断标志,一旦检测到标志置位,即表示串口接收完一包数据,开始进行处理,将接收到的数据再通过发送函数 HAL_UART_Transmit_DMA()函数发送出来,然后再开启下一次的串口DMA接收。
在这里插入图片描述

		if(clear_flag_DMA){																			  /* 判断是否接收到数据  */ 
			HAL_UART_Transmit_DMA(&huart1, (uint8_t *)RxBuffer, 9); /* 将接收到的数据再发送出去 */ 
			HAL_UART_Receive_DMA(&huart1, (uint8_t *)RxBuffer, 9);  /* 开启串口中断接收数据  */ 
		  clear_flag_DMA = 0;																		  /* 将接收数据标志清0,等待下一次接收数据  */ 
		}

(2)回调函数设置
  在中断回调函数里面不进行过多的设置,因为此时还在中断中,应该遵循“快进快出”的原则,所以只是在回调中将标志位置1,在主循环中进行判断标志位从而进行数据处理的。
在这里插入图片描述

/* DMA接收完成回调函数 */ 
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1) /* 判断是不是串口1产生的中断 */
  {
		clear_flag_DMA = 1;					/* 将接收标志置1 */
  }
}

5.3.4 示例结果

  打开串口助手,在发送栏输入发送“123456789”,发送后会接受到一条同样的内容,具体示例如下:
在这里插入图片描述
  现在已经把串口的阻塞收发、中断收发、DMA收发全部介绍一遍了,到这里已经解决大部分的任务了,不过可能还会遇到一个情况,上面的示例中都是对固定字长的数据进行接收,都是用的“123456789”,在实际的应用中,不排除接收不定长数据的要求,那么什么方式是处理不定长数据比较好的方式呢?
  是的,还有一种方式,空闲中断+DMA的,这种方式也是工作中用的比较多的方式,也是本人刚开始工作时一位同事传授的方法,同样也是这位同事带我迈进嵌入式的门槛,得到了很多工作中的经验,在此对这位同事表示感谢!遇到一个好的师傅是一个相当幸运的事,希望屏幕前读者也有能获得这样的机会,即使没有,也应勤学多练,做大做强,再创辉煌。

5.4 串口 空闲中断+DMA方式接收数据

5.4.1 原理概述

  串口空闲中断是一种检测串口通信中无数据传输时的机制。其原理是,当串口接收线在一段时间内保持在空闲电平(通常是稳定的高电平或低电平),系统会触发一个中断。这个中断信号通知处理器串口当前没有数据流动,可以执行数据处理或优化操作。例如,处理器可以读取缓冲区中的数据,清空接收缓冲区,或进入低功耗模式,从而提高系统效率和节省资源。

5.4.2 STM32CubeMX配置

   空闲中断+DMA方式的CubeMx配置和串口的DMA配置是一样的,在不再赘述。

5.4.3 程序设置

   在串口DMA程序的基础上需要进行下面的操作。
(1)
①首先定义用到的变量,包括存储接收到数据的长度、接收数据用到的数组,发送数据用到的数组和接收到数据用的标志。
在这里插入图片描述

uint8_t RxLen = 0;							 /* 接收到数据的长度 */
uint8_t TxBuffer[100] = {0};		 		 /* 发送串口数据的数组 */
uint8_t RxBuffer[100] = {0};  	 			 /* 接收串口数据的数组 */
uint8_t clear_flag_DMA_IDLE = 0; 			 /* 接收到数据标志 */

  ② 因为这几个变量在其他源文件中也被调用,所以需要在main.h中进行声明,声明时不应该带有变量的赋值,具体声明如下:
在这里插入图片描述

extern uint8_t RxLen;									 /* 接收到数据的长度 */
extern uint8_t TxBuffer[100];					 		 /* 发送串口数据的数组 */
extern uint8_t RxBuffer[100];  	 			             /* 接收串口数据的数组 */
extern uint8_t clear_flag_DMA_IDLE;	 	 				 /* 接收到数据标志 */

(2)
①需要在串口中断服务函数里面添加如下代码:
在这里插入图片描述

	if((__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) != RESET)){    	/* 判断idle标志被置位 */
		__HAL_UART_CLEAR_IDLEFLAG(&huart1);                         /* 清除标志位 */
		HAL_UART_DMAStop(&huart1);                                  /* 停止DMA传输 */
		RxLen = 100 -__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);	    /* 获取数据长度 */
		memcpy(TxBuffer,RxBuffer,RxLen);             				/* 获取接收到的数据 */
		clear_flag_DMA_IDLE = 1;								    /* 将接收到的数据放入结构体 */
		HAL_UARTEx_ReceiveToIdle_DMA(&huart1,RxBuffer,100);         /* 重新打开空闲中断串口DMA接收 */
	}

因为上面的逻辑是串口空闲中断相对重要的部分,所以在这里逐句对其分析:

  • if((__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) != RESET))
    检查UART是否处于空闲状态,当串口接收完一帧数据后,会进入空闲状态,通过检查空闲标志位可以确定接收完成。

  • __HAL_UART_CLEAR_IDLEFLAG(&huart1);
    清除空闲标志,为了防止后续接收过程被误判为空闲状态,需要清除空闲标志位,确保下一次能够正确检测到空闲状态。

  • HAL_UART_DMAStop(&huart1);
    该函数是在检测到 UART 空闲中断(IDLE Interrupt)后停止 DMA 传输,以确保接收缓冲区中的数据不再被新的数据覆盖,能够准确计算接收的数据长度,并安全地处理这些数据。

  • RxLen = 100 -__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
    计算已经接收到的数据长度。本次示例中DMA缓冲区长度设置为100,通过减去DMA计数器的当前值(表示剩余的未接收字节数)来得到接收到的数据长度。

  • memcpy(TxBuffer,RxBuffer,RxLen);
    将接收到的数据从接收缓冲区(RxBuffer)复制到发送缓冲区(TxBuffer),以便后续处理。

  • clear_flag_DMA_IDLE = 1;
    设置标志位,指示已经处理了一个空闲中断。用于通知其他代码接收到数据并完成了处理。

  • HAL_UARTEx_ReceiveToIdle_DMA(&huart1,RxBuffer,100);
    重新启动UART的DMA接收功能,准备接收下一批数据。

② 配置好上面的代码后,会发现memcpy函数会报错,memcpy是一个标准库函数,用于从一个内存位置复制数据到另一个位置。包含“string.h”头文件是必要的,因为该文件声明了 memcpy 函数及其他相关的内存操作和字符串操作函数,使其在代码中可用。
memcpy函数原型:

void *memcpy(void *dest, const void *src, size_t n);

参数说明

  • dest :指向目标内存区域的指针,表示数据将被复制到这个区域。
  • src :指向源内存区域的指针,表示数据将从这个区域复制。
  • n :要复制的字节数。

在这里插入图片描述

(3)
① 然后回到main.c源文件里面,在源文件里面先添加启动 UART 的 DMA 接收,并在接收到数据结束时触发空闲中断(IDLE Interrupt)。

  HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RxBuffer, 100);

在这里插入图片描述
②在while循环里面处理接收到的数据,本次在while循环里面首先将接收到的数据发送出去,然后将接收标志置0,表示本次已经处理完一次接收到的数据。如果需要对接收到的数据做其他的处理,可以在这个主循环里面进行添加,如果采用了RTOS操作系统,则可以将这一部分转移至一个线程中进行处理。

		if(clear_flag_DMA_IDLE){																			  /* 判断是否接收到数据  */ 
			HAL_UART_Transmit_DMA(&huart1, (uint8_t *)RxBuffer, RxLen);   /* 将接收到的数据再发送出去 */ 		
		  clear_flag_DMA_IDLE = 0;																		  /* 将接收数据标志清0,等待下一次接收数据  */ 
		}

5.4.4 程序设置

  打开串口助手,在发送栏输入发送“123456789”,发送后会接受到一条同样的内容,具体示例如下:
在这里插入图片描述

六、总结

  本次示例中记录了单片机串口驱动的原理,串口使用的四种方式,轮询、中断、DMA、DMA+空闲中断,以及每种方式的原理,串口在日常调试中还是使用较多的,可以多掌握几种串口调试的方式。


项目源码:HAL库STM32常用外设教程(三)—— USART/UART通信
参考书籍和文章:
1、《STM32Cube高效开发教程(基础篇)》王维波
2、《STM32F4xx中文参考手册》
3、《STM32F407 探索者开发指南》
4、IIC、SPI、USART、RS485、RS232、CAN外设通信总结
5、对单片机中断的详细解析
6、stm32使用HAL库配置串口中断收发数据(保姆级教程)


一以贯之的努力,不得懈怠的人生,每天的微小积累,会决定最终结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值