初探STM32F4(2)--USART(1)


本文是对STM32的USART相关知识扫盲(参考正点原子的教学),文章架构如下:

  1. 结合《STM32F4xx中文参考手册》,先从系统框图和寄存器的角度剖析USART硬件架构。
  2. 然后结合库例程剖析USART初始化的基本配置流程,进一步学习嵌入式代码规范

阅读完本文,要能回答以下问题:

  1. 什么是USART?跟USART并列的串行通信标准还有哪些?
  2. 采用USART实现不同对象之间通讯时,需要注意什么
  3. 简述USART模块系统框图构成?如何让USART模块工作在不同模式?
  4. USART模块是用来收发数据的,而正常收发数据的进行,是需要设置合适的波特率的,结合硬件知识谈谈对设置波特率的理解。
  5. 如何计算并配置波特率相关的寄存器呢?
  6. 简述USART的相关寄存器,并对寄存器中的常用位进一步解释。

HAL库是通过HAL_UART_Init()函数进行USART相关寄存器的初始化配置的,研究此函数,回答以下问题:

  1. 从该函数接收参数、返回参数的角度简述该函数是如何配置串口初始化的。
  2. 需要进行初始化配置的USART的寄存器的地址是如何定位到的?
  3. 从该函数具体实现何种功能的角度简述该函数是如何配置串口初始化的。
  4. 该函数里面,如何初始化串口相关IO口的复用配置的?
  5. 上一个问题我们已经知道,该函数通过调用HAL_UART_MspInit()函数来实现IO口的复用配置,进一步研究可以发现,这个函数在定义时,函数类型前面可以加了_Weak关键字,这个关键字有什么用?
  6. 该函数里面,如何初始化串口特性的呢?

一、USART概述

1、什么是USART?

  • USART全称通用同步/异步串行接收/发送器,它是一种全双工、同步/异步、串行的通信设备
  • 全双工/半双工/单工是一组概念,表征数据的传送方向。单工是指数据只支持在一个方向传输,半双工是指数据可以两个方向传输、但同一时刻只能有一个方向,全双工允许统一时刻两个方向传输
  • 同步/异步是一组概念,表征数据传送是否依靠时钟信号来同步。带时钟信号传输就是同步通信、不带时钟信号就是异步通信
  • 串行/并行是一组概念,数电基础中已讲过移位寄存器的概念,不再赘述。

2、与USART并列的串行通信标准还有哪些?
在这里插入图片描述
3、采用USART实现不同对象之间通讯,需要注意什么

  • 不同终端之间通过USART通信,双方要注意适配问题,有三个层次的适配问题需要注意
  • 第一个层次是物理层,即接口适配,常用的接口有RS232、RS485、TTL直连、USB
    在这里插入图片描述
  • 第二个层次是数据层,即规定好数据格式
    • 包括起始位(1位逻辑0表示数据位开始)、数据位(9或9位)、奇偶校验位、停止位(1或2位)
    • 波特率规定了移位寄存器传输每一位数据的时钟速度
  • 第三个层次是协议层,即程序规定好通讯规范

二、USART硬件架构

系统框图

在这里插入图片描述
1、简述USART模块系统框图构成

  • 当需要接收数据时,外部数据从RX引脚串行输入到IrDA(一个红外线数据通信模块),然后送入接收移位寄存器,在数电一章我们已经了解过,移位寄存器就是一系列的D触发器的组合,它支持串入、串出、并入、并出。当一组数据完整送入到移位寄存器后,再并出到数据寄存器(RDR)。
  • 对于发送数据,数据总线先把数据送到发送数据寄存器(TDR),并入到发送移位寄存器,然后从TX接口串行输出。
  • 对于上述数据收发过程,存在大量的组合逻辑电路在配合工作。
    • 对於单独一块电路的正常工作,需要配置好它的控制信号
    • 对于多块电路协调工作时,当某一块电路已经完成了工作,需要产生标志信号,此时可以使能下一块电路开始工作。
    • 同时,当某些特定模块产生标志信号后,表明系统已经完成了某些功能,CPU可以接手做一些信息处理了(进入中断服务程序)
    • 这些零零总总的信号构成了USART的各种寄存器。

2、通过分析系统框图,可以发现USART可以工作在不同的模式,简单阐述下其中的机理。

  • USART模块可以配置成多种模式,具体能配置的模式如下所示。
    在这里插入图片描述
  • USART不同模式怎么选择呢?每一种模式对应一块电路需要工作,只要把对应模式的电路给使能信号即可。使能信号也好理解,必然是对应于寄存器中的某些位啦。

3、利用数电知识分析如何设置波特率?

  • 设置波特率就是设置移位寄存器的时钟频率,这个时钟频率决定了一组数据完整送入进移位寄存器,可以并行输出至数据寄存器(DR)的时间。只有双方约定好了这个时间,才能正确地对数据进行解析。
  • 设置波特率大小的直接思路就是对系统高频时钟信号进行分频,得到一个频率相对较低的时钟频率。
  • 如何分频?利用计数器就能实现分频功能。
  • 使用计数器实现分频需要的控制信号有哪些?需要设置计数器每个周期复位的门槛值,这个值可以被映射成时钟频率的分频倍数
  • 分频倍数对应USART寄存器USARTDIV中存储的值,这个值可以通过软件赋值。

波特率计算过程

1、如何计算波特率,即正确的分频值呢?

  • 已经知道,波特率设置就是对高频时钟进行分频。而高频时钟的来源有两处:fPCLK1和fPCLK2。
  • 对高频时钟进行两步分频:
    • 先经过USARTDIV分频
    • 再经过采样除法器分频。采样除法器可以实现16倍分频过采样和8倍分频过采样
  • USARTDIV分频系数的取值,是通过波特率寄存器USART_BRR来设置的,USART_BRR低16位有效,且分为两部分,高12位用于设置整数,低四位用于设置小数。当采样除法器8倍分频时,小数位的最高位无效。
  • 举一个例子:
    • 要设置115200波特率,在10进制下,用高频时钟频率除以需要设置的波特率,再除以采样除法器的16倍分频(默认over=0),得到USARTDIV的值为48.8
    • 将SARTDIV的整数部分赋值给DIV_Mantissa,即整数部分写入0X30
    • 将SARTDIV的小数部分乘16赋值给DIV_Fraction(乘16是因为小数部分是一个占几分之几的概念),即小数部分写入0X0D
    • 所以USART_BRR寄存器写入0X30D。

USART相关寄存器

1、简述USART相关寄存器,如下所示。

  • 首先是上文已经介绍过的波特率寄存器USART_BRR
    在这里插入图片描述
  • 每个USART串口有三个控制寄存器,对应USARTx_CR1~3,具体每一位的含义查数据手册。
    • USARTx_CR1用于定义数据格式,使能USART收发数据,使能是否响应各种中断信号
      在这里插入图片描述
    • USARTx_CR2用于定义USART模块的工作模式
      在这里插入图片描述
    • USARTx_CR3也用于定义USART模块的工作模式
      在这里插入图片描述
  • 总的来说,USARTx_CR1最为重要,详细介绍一下,低16位有效:
    • OVER8:过采样模式选择
    • UE:USART使能,软件置位
    • M:选择字长
    • WAKE:决定USART的唤醒方式
    • PCE:Parity control enable,置1使能后,计算出来的奇偶校验位(可以通过组合逻辑电路硬件实现)被插入到MSB(Most Significant Bit)最高有效位,并对接收到的数据检查奇偶校验位。
    • PS:Parity Selection,设置是奇校验还是偶校验
    • 9~4位是各种标志符的中断使能(IE、Interrupt enable)信号
    • TE、RE:发送寄存器与接收寄存器的使能
    • RWU:Receiver WakeUp,选择USART接收器是否工作在静音模式
    • SBK:Send Break,用于发送断路字符
  • 接着是两个数据寄存器
    低9位有效,数据寄存器包含两个寄存器,一个用于发送(TDR)、一个用于接收(RDR)。当奇偶校验位使能时,RDR的MSB位送入到一个组合逻辑电路进行奇偶校验
  • 最后是一个状态寄存器USART_SR
    在这里插入图片描述
    • 低10位有效,如同前文所述,当USART某些模块完成既定功能后,由输出一个标志位(硬件置1)。用户可以进行后续操作了。
    • 如果标志位对应的中断使能了,则会进入中断服务。
    • 进行完既定操作后,通过软件写操作或者硬件自动将各标志位清0。
  • 介绍常用的几个状态标志位
    • TXE,发送数据寄存器为空。当TDR寄存器的内容已传输到移位寄存器时,该位由硬件置1。如果TXEIE中断使能置位了,则会进入中断服务,后续软件清零。
    • TC,发送完成,如果已经对数据帧完整发送并且TXE为1时,则该位由硬件置1。如果TCIE中断使能置位了,则会进入中断服务,后续软件清零。
    • RXNE,读取数据寄存器不为空。当移位寄存器中的内容已经传输到RDR时,该位由硬件置1。如果相应中断使能了,则会进入中断服务,后续软件清零。

三、USART初始化代码剖析

本节基于HAL库架构,剖析USART初始化的代码细节,USART初始化的代码如下,代码结构简单易懂,下面开始具体分析:

UART_HandleTypeDef UART1_Handler; //声明串口1的配置特性
void uart_init(u32 bound)
{	
	//UART 初始化设置
	UART1_Handler.Instance=USART1;					    //表明配置USART1
	UART1_Handler.Init.BaudRate=bound;				    //配置波特率
	UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B;   //配置字长为8位
	UART1_Handler.Init.StopBits=UART_STOPBITS_1;	    //一位停止位
	UART1_Handler.Init.Parity=UART_PARITY_NONE;		    //无奇偶校验位
	UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE;   //无硬件流控制
	UART1_Handler.Init.Mode=UART_MODE_TX_RX;		    //收发模式
	HAL_UART_Init(&UART1_Handler);					  //该函数初始化串口
//......

}

1、使用HAL_UART_Init()函数进行USART相关寄存器的初始化配置,从该函数接收参数、返回参数的角度简述该函数是如何配置串口初始化的。

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart){}
  • 首先分析该函数的返回参数
    函数的返回类型为HAL_StatusTypeDef,它是用枚举类型来定义的,具体代码如下,它定义了函数的四种返回状态,可以根据返回值判断函数的执行状态。
typedef enum 
{
  HAL_OK       = 0x00,
  HAL_ERROR    = 0x01,
  HAL_BUSY     = 0x02,
  HAL_TIMEOUT  = 0x03
} HAL_StatusTypeDef;
  • 然后分析该函数的入口参数,入口参数类型为UART_HandleTypeDef,其结构体成员变量如下所示:
typedef struct
{
  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              */
  
  uint16_t                      TxXferCount;      /*!< UART Tx Transfer Counter           */
  
  uint8_t                       *pRxBuffPtr;      /*!< Pointer to UART Rx transfer Buffer */
  
  uint16_t                      RxXferSize;       /*!< UART Rx Transfer size              */
  
  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    State;            /*!< UART communication state           */
  
  __IO uint32_t                 ErrorCode;        /*!< UART Error code                    */

}UART_HandleTypeDef;
  • UART_HandleTypeDef成员变量包括以下几组变量
    • USART_TypeDef *Instance,指向目标串口的基地址
    • UART_InitTypeDef Init,是目标串口需要初始化配置的特性
    • 与发送数据相关的三个变量,*pTxBuffPtr、TxXferSize、TxXferCount
    • 与接收数据相关的三个变量,*pRxBuffPtr、 RxXferSize、RxXferCount(这些变量的作用后续使用时,再详细解释)
    • DMA相关设置变量
    • LOCK相关设置变量
    • UART的状态标志位
    • UART错误日志
  • 容易知道:
    • 通过配置Init的成员变量,从而配置好目标串口的具体初始化特性
    • Instance指针是指向结构体USART_TypeDef,通过给Instance赋值,选择需要配置的串口地址
    • 然后将Init的成员变量赋值给Instance的成员变量,实现串口初始化。

2、需要初始化配置的USART的寄存器的地址是如何定位到的?

  • 指针Instance指向的地址取值为USART1 ~ x,USART1~x的地址是通过外设总线地址加偏移量定义的,然后一步一步映射,通过GPIO那一节的文章我们知道,最终是通过RAM里面的某一个绝对地址作为基地址。
#define USART1              ((USART_TypeDef *) USART1_BASE)
#define USART6              ((USART_TypeDef *) USART6_BASE)

#define USART1_BASE           (APB2PERIPH_BASE + 0x1000)
#define USART6_BASE           (APB2PERIPH_BASE + 0x1400)
  • 串口地址已经定位好了,再看看Instance指针指向的结构体USART_TypeDef的成员变量,由此我们知道,通过访问Instance的成员变量,定位到相应串口配置寄存器的地址的。
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;

3、使用HAL_UART_Init()函数进行USART相关寄存器的初始化配置,从该函数具体实现功能的角度简述该函数是如何配置串口初始化的。

  • HAL_UART_Init()函数要实现两个初始化功能,第一个功能是初始化串口相关IO口的复用配置
  • 第二个功能才是初始化串口相关特性,将Init的成员变量赋值给Instance的成员变量来实现。

4、HAL_UART_Init()函数里面,如何初始化串口相关IO口的复用配置的?

  • 稍微研究下就可以发现,HAL_UART_Init()函数通过调用HAL_UART_MspInit()函数,实现了串口相关IO口的复用配置,相关代码如下,GPIO口的相关配置细节不再赘述。
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    //GPIO端口设置
	GPIO_InitTypeDef GPIO_Initure;
	
	if(huart->Instance==USART1)//配置USART1
	{
		__HAL_RCC_GPIOA_CLK_ENABLE();			//串口1使用到的是PA9和PA10。所以使能GPIOA
		__HAL_RCC_USART1_CLK_ENABLE();			//然后使能UART1
	
		GPIO_Initure.Pin=GPIO_PIN_9;			//PA9
		GPIO_Initure.Mode=GPIO_MODE_AF_PP;		//GPIO工作在复用模式
		GPIO_Initure.Pull=GPIO_PULLUP;			//上拉
		GPIO_Initure.Speed=GPIO_SPEED_FAST;		//高速
		GPIO_Initure.Alternate=GPIO_AF7_USART1;	//复用成哪一种复用模式?
		HAL_GPIO_Init(GPIOA,&GPIO_Initure);	   	//将原始寄存器设置成以上特性
		GPIO_Initure.Pin=GPIO_PIN_10;			//PA10
		HAL_GPIO_Init(GPIOA,&GPIO_Initure);	   	//PA10设置成以上特性
		
#if EN_USART1_RX
		HAL_NVIC_EnableIRQ(USART1_IRQn);		//使能中断
		HAL_NVIC_SetPriority(USART1_IRQn,3,3);	//设置中断优先级
#endif	
	}

}

5、我们已经知道,调用HAL_UART_MspInit()函数来实现IO口的复用配置,进一步研究可以发现,这个函数在定义时,函数类型前面可以加了_Weak关键字,这个关键字有什么用?

  • 官方库文件对HAL_UART_MspInit()如下定义,即在函数里面什么事都不干。
 __weak void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
   /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);
  /* NOTE: This function Should not be modified, when the callback is needed,
           the HAL_UART_MspInit could be implemented in the user file
   */ 
}
  • 上文我们用户自己对HAL_UART_MspInit()重新定义了一番,这样难道不会造成重定义报错么?
  • _Weak关键字定义的函数,允许用户重新定义一个同名函数,最终编译器编译的时候会选择用户定义的函数。如果用户没有定义,那么才执行_Weak关键字定义的函数内容。
  • 这样做有什么好处呢?
    开发者可以事先定义好一个流程,后续如果项目需求改了,不用对既定流程做任何修改,只用对流程中的与实现关联的代码细节进行修改即可。
typedef struct
{
  uint32_t BaudRate;                  /*!< This member configures the UART communication baud rate.
                                           The baud rate is computed using the following formula:
                                           - IntegerDivider = ((PCLKx) / (8 * (OVR8+1) * (huart->Init.BaudRate)))
                                           - FractionalDivider = ((IntegerDivider - ((uint32_t) IntegerDivider)) * 8 * (OVR8+1)) + 0.5 
                                           Where OVR8 is the "oversampling by 8 mode" configuration bit in the CR1 register. */

  uint32_t WordLength;                /*!< Specifies the number of data bits transmitted or received in a frame.
                                           This parameter can be a value of @ref UART_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 fPCLK/8).
                                           This parameter can be a value of @ref UART_Over_Sampling */ 
}UART_InitTypeDef;

6、HAL_UART_Init()函数如何初始化串口特性的呢?
容易发现,HAL_UART_Init()通过调用UART_SetConfig()函数,实现了串口特性的初始化。UART_SetConfig()函数配置寄存器的流程如下:

  1. 检查参数是否有问题?
  2. 将待配置寄存器读出来。
  3. 将待配置寄存器的相关位清零。
  4. 配置相关位。
  5. 将配置好的值写回寄存器
static void UART_SetConfig(UART_HandleTypeDef *huart)
{
  /* Check the parameters */
  assert_param(IS_UART_BAUDRATE(huart->Init.BaudRate));  
  assert_param(IS_UART_STOPBITS(huart->Init.StopBits));
  //......

  /*-------------------------- USART CR1 Configuration -----------------------*/
  tmpreg = huart->Instance->CR1;

  /* Clear M, PCE, PS, TE and RE bits */
  tmpreg &= (uint32_t)~((uint32_t)(USART_CR1_M | USART_CR1_PCE | USART_CR1_PS | USART_CR1_TE | \
                                   USART_CR1_RE | USART_CR1_OVER8));

  /* Configure the UART Word Length, Parity and mode: */
  tmpreg |= (uint32_t)huart->Init.WordLength | huart->Init.Parity | huart->Init.Mode | huart->Init.OverSampling;

  /* Write to USART CR1 */
  huart->Instance->CR1 = (uint32_t)tmpreg;
}

7、这里会产生一个疑惑,Init的成员变量都是32位的数据类型,但是,是要去配置寄存器中对应的某些位,这是否会有矛盾?

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;

不矛盾,Init的成员变量虽然都是32位的数据类型,但是只有特定位才会置数,其余位都是置零的,因此这里将Init的多个成员变量相或,多个成员变量之间相互不影响,共同组成一个总的32位的数据,再赋值给串口相关控制寄存器。

分析至此,我们已经知道串口初始化最重要的两步是怎么进行的:

  1. 串口IO口复用配置。
  2. 串口相关特性配置。

知道了这两点,我们可以说是已经会用串口了,但远远谈不上会写串口配置代码。HAL_UART_Init()函数里面还涉及大量代码运行逻辑,笔者本文不会进一步深入了,感兴趣的读者自行研究。

但有一点私货想跟读者分享,不要过分追求学究思维中的完美主义,不要对所有细节都企图了解的面面俱到,我们要追求工程师思维,即

  1. 先实现产品需求,一上来先做到从无到有,再慢慢从有到好。
  2. 但不要止步于从无到有的短暂快乐,从有到好的痛苦加班过程才更有意义
  3. 实现从有到好的过程,即对种种细节进行优化的过程,我们也要对各种细节赋予权重,权重高的细节优先研究,权重低的细节适当舍弃。不要有强迫症思想。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章