初探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. 實現從有到好的過程,即對種種細節進行優化的過程,我們也要對各種細節賦予權重,權重高的細節優先研究,權重低的細節適當捨棄。不要有強迫症思想。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章