【STM32F407開發板使用者手冊】第23章 STM32F407的USART串列埠基礎知識和HAL庫API
最新教程下載:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255
第23章 STM32F407的USART串列埠基礎知識和HAL庫API
本章節為大家講解USART(Universal synchronous asynchronous receiver transmitter,通用同步非同步收發器)的基礎知識和對應的HAL庫API。
23.1 初學者重要提示
23.2 串列埠基礎知識
23.3 串列埠的HAL庫用法
23.4 原始檔stm32f4xx_hal_uart.c
23.5 總結
23.1 初學者重要提示
- 學習串列埠外設推薦從硬體框圖開始瞭解基本的功能特性,然後逐步深入瞭解各種特性,這種方式方便記憶和以後查閱。而串列埠的通訊學習,推薦看時序圖。
- 部分中斷標誌是可以通過操作傳送資料暫存器TDR或者接收資料暫存器RDR實現清除,這點要特別注意,詳情看本章23.3.4小節。
- 初次使用USART,還是有不少注意事項的,詳情看本章23.3.3小節和23.4.1小節。
23.2 串列埠基礎知識
USART的全稱是Universal synchronous asynchronous receiver transmitter,中文意思是通用同步非同步收發器。我們經常使用串列埠是非同步串列埠,簡稱UART。
23.2.1 串列埠的硬體框圖
認識一個外設,最好的方式就是看它的框圖,方便我們快速的瞭解串列埠的基本功能,然後再看手冊瞭解細節。
通過這個框圖,我們可以得到如下資訊:
- TX和RX介面
分別用於資料的傳送和接收。
- SW_RX介面
在智慧卡模式下,此介面用於接收資料。
- IRAD_OUT和IRAD_IN介面
用於IRAD模式的資料傳送和接收。
- RST和CTS介面
用於硬體流控制。
- USART_BRR
波特率生成單元。
- 傳送過程經過的暫存器
依次是USART_TDR -> TxFIFO ->Tx Shift Reg偏移暫存器 –> TX或者RX引腳。
- 接收經過的暫存器
依次是TX或者RX引腳-> Rx Shift Reg偏移暫存器->RxFIFO –>USART_RDR。
23.2.2 串列埠的基本功能
STM32的串列埠功能很強大,支援太多的模式。我們只需關心我們最常用的特性即可。我們的串列埠驅動使用的串列埠中斷+FIFO結構,沒有使用DMA。因此我們只討論和串列埠中斷、串列埠常規引數有關的知識。
STM32串列埠的優越特性:(只列了舉常用的)
- 任意波特率。硬體採用分數波特率發生器系統,可以設定任意的波特率,最高達11.25Mbits/s。這一點很重要。比如ES8266串列埠WIFI晶片,上電時有個奇怪的波特率74880bps,當然STM32是可以支援的。
- 可程式設計資料字長度,支援8bit和9bit。
- 可配置的停止位。支援1或2個停止位。
- 傳送器和接收器可以單獨使能。比如GPS應用只需要串列埠接收,那麼傳送的GPIO就可以節省出來用作其他功能。
- 檢測標誌和中斷:
a. 接收緩衝器滿,可產生中斷。串列埠中斷服務程式據此判斷是否接收到資料。
b. 傳送緩衝器空,可產生中斷。串列埠中斷服務程式據此啟動傳送下一個資料。
c. 傳輸結束標誌,可產生中斷。用於RS485通訊,等最後一個位元組傳送完畢後,需要控制RS485收發器晶片切換為接收模式。
其它中斷不常用,包括:CTS改變、LIN斷開符檢測、檢測到匯流排為空閒(在DMA不定長接收方式會用到)、溢位錯誤、幀錯誤、噪音錯誤、校驗錯誤。
23.2.3 不同串列埠支援的特性異同
通過下面的表格,可以對串列埠1-8支援的功能有個全面的認識:
23.2.4 串列埠的資料幀格式
串列埠支援的幀格式如下(M和PCE都是USART_CR1暫存器的位,其中M位用於控制幀長度,PCE用於使能奇偶校驗位):
這裡特別注意奇偶校驗位,使用者在配置的時候可以選擇奇校驗和偶校驗,校驗位是佔據的最高位。比如選擇M=00,PCE=1,即7bit的資料位。
- 串列埠傳送資料:
如果傳送的7bit資料是111 0011,這個裡面有奇數個1,那麼選擇偶校驗的情況下,校驗位 = 1,湊夠偶數個1,而選擇奇校驗的情況下,校驗位 = 0,因為已經是奇數個1。校驗位不需要使用者去計算,是硬體自動生成的。
- 串列埠接收資料:
根據使用者設定的奇校驗或者偶校驗型別,串列埠硬體會對接收到的資料做校驗,如果失敗,USART_ISR暫存器的PE位會被置1。如果使能了對應的中斷PEIE,那麼失敗的時候還會產生中斷。
瞭解到幀格式後,再來看一下實際資料傳送時,資料位的先後順序:
23.2.5 串列埠傳送時序圖
這個時序圖非常具有代表性,可以幫助大家很好的理解TC傳送完成中斷和TXE空中斷。
23.2.6 同步串列埠和非同步串列埠的區別
非同步通訊是按字元傳輸的。每傳輸一個字元就用起始位來進行收、發雙方的同步,不會因收發雙方的時鐘頻率的小的偏差導致錯誤。這種傳輸方式利用每一幀的起、止訊號來建立傳送與接收之間的同步。
非同步的特點是:每幀內部各位均採用固定的時間間隔,而幀與幀之間的間隔是隨機的。接收機完全靠每一幀的起始位和停止位來識別字符是正在進行傳輸還是傳輸結束。
同步通訊的傳送和接收雙方要保持完全的同步,因此要求接收和傳送裝置必須使用同一時鐘。優點是可以實現高速度、大容量的資料傳送;缺點是要求發生時鐘和接收時鐘保持嚴格同步,同時硬體複雜。
可以這樣說,不管是非同步通訊還是同步通訊都需要進行同步,只是非同步通訊通過傳送字元內的起始位來進行同步,而同步通訊採用共用外部時鐘來進行同步。所以,可以說前者是自同步,後者是外同步。
23.2.7 單工,半雙工和全雙工通訊
單工:在一個單工的序列通訊系統中,一般至少有兩根線(訊號線和地線),資料傳送只有一個方向,例如可以使用單工資料傳送將資料從一個簡單的資料監測系統傳送到PC上。
半雙工:在半雙工序列通訊系統中,一般同樣要求至少有兩根線。這裡的資料傳送是雙向的。然而,同一個時刻只能為一個方向。在上面的資料監測的例子中做了一些變化,可以使用半雙工通訊機制傳送資訊到嵌入式模組(來設定引數,比如取樣率)。此外,在其他時候,可以使用這個種連線將嵌入式裝置上的資料下載到PC中。
全雙工:在一個全雙工的序列通訊系統中,一般要求至少有三根線(訊號線A,訊號線B和地線)。訊號線A將傳輸一個方向上的資料,同時訊號線B傳送另一個方向上的資料。
23.3 串列埠的HAL庫用法
串列埠的HAL庫用法其實就是幾個結構體變數成員的配置和使用,然後配置GPIO、時鐘,並根據需要配置NVIC、中斷和DMA。下面我們逐一展開為大家做個說明。
23.3.1 串列埠暫存器結構體USART_TypeDef
USART相關的暫存器是通過HAL庫中的結構體USART_TypeDef定義的,在stm32f4xx.h中可以找到這個型別定義:
typedef struct { __IO uint32_t SR; /*!< USART Status register, Address offset: 0x00 */ __IO uint32_t DR; /*!< USART Data register, Address offset: 0x04 */ __IO uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */ __IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */ __IO uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */ __IO uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */ __IO uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */ } USART_TypeDef;
這個結構體的成員名稱和排列次序和CPU的USART暫存器是一 一對應的。
__IO表示volatile, 這是標準C語言中的一個修飾字,表示這個變數是非易失性的,編譯器不要將其優化掉。core_m4.h檔案定義了這個巨集:
#define __O volatile /*!< Defines 'write only' permissions */ #define __IO volatile /*!< Defines 'read / write' permissions */
下面我們看下USART1、USART2 ... UART8的定義,在stm32f4xx.h檔案
#define PERIPH_BASE 0x40000000UL #define APB1PERIPH_BASE PERIPH_BASE #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000UL) #define USART1_BASE (APB2PERIPH_BASE + 0x1000UL) #define USART6_BASE (APB2PERIPH_BASE + 0x1400UL) #define USART2_BASE (APB1PERIPH_BASE + 0x4400UL) #define USART3_BASE (APB1PERIPH_BASE + 0x4800UL) #define UART4_BASE (APB1PERIPH_BASE + 0x4C00UL) #define UART5_BASE (APB1PERIPH_BASE + 0x5000UL) #define UART7_BASE (APB1PERIPH_BASE + 0x7800UL) #define UART8_BASE (APB1PERIPH_BASE + 0x7C00UL) #define USART1 ((USART_TypeDef *) USART1_BASE) <----- 展開這個巨集,(USART_TypeDef *) 0x40001010 #define USART2 ((USART_TypeDef *) USART2_BASE) #define USART3 ((USART_TypeDef *) USART3_BASE) #define UART4 ((USART_TypeDef *) UART4_BASE) #define UART5 ((USART_TypeDef *) UART5_BASE) #define USART6 ((USART_TypeDef *) USART6_BASE) #define UART7 ((USART_TypeDef *) UART7_BASE) #define UART8 ((USART_TypeDef *) UART8_BASE)
我們訪問USART1的CR1暫存器可以採用這種形式:USART1->CR1 = 0。
23.3.2 串列埠控制代碼結構體UART_HandleTypeDef
HAL庫在USART_TypeDef的基礎上封裝了一個結構體UART_HandleTypeDef,定義如下:
typedef struct __UART_HandleTypeDef { USART_TypeDef *Instance; /*!< UART registers base address */ UART_InitTypeDef Init; /*!< UART communication parameters */ uint8_t *pTxBuffPtr; /*!< Pointer to UART Tx transfer Buffer */ uint16_t TxXferSize; /*!< UART Tx Transfer size */ __IO uint16_t TxXferCount; /*!< UART Tx Transfer Counter */ uint8_t *pRxBuffPtr; /*!< Pointer to UART Rx transfer Buffer */ uint16_t RxXferSize; /*!< UART Rx Transfer size */ __IO uint16_t RxXferCount; /*!< UART Rx Transfer Counter */ DMA_HandleTypeDef *hdmatx; /*!< UART Tx DMA Handle parameters */ DMA_HandleTypeDef *hdmarx; /*!< UART Rx DMA Handle parameters */ HAL_LockTypeDef Lock; /*!< Locking object */ __IO HAL_UART_StateTypeDef gState; /*!< UART state information related to global Handle management and also related to Tx operations. This parameter can be a value of @ref HAL_UART_StateTypeDef */ __IO HAL_UART_StateTypeDef RxState; /*!< UART state information related to Rx operations. This parameter can be a value of @ref HAL_UART_StateTypeDef */ __IO uint32_t ErrorCode; /*!< UART Error code */ #if (USE_HAL_UART_REGISTER_CALLBACKS == 1) void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart); void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart); void (* ErrorCallback)(struct __UART_HandleTypeDef *huart); void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart); void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart); void (* WakeupCallback)(struct __UART_HandleTypeDef *huart); void (* MspInitCallback)(struct __UART_HandleTypeDef *huart); void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart); #endif } UART_HandleTypeDef;
這裡重點介紹前兩個引數,其它引數主要是HAL庫內部使用的。
- USART_TypeDef *Instance
這個引數是暫存器的例化,方便操作暫存器,比如使能串列埠的傳送空中斷。
SET_BIT(huart->Instance->CR1, USART_CR1_TXEIE)。
- UART_InitTypeDef Init
這個引數是使用者接觸最多的,用於配置串列埠的基本引數,像波特率、奇偶校驗、停止位等。UART_InitTypeDef結構體的定義如下:
typedef struct { uint32_t BaudRate; /* 波特率 */ uint32_t WordLength; /* 資料位長度 */ uint32_t StopBits; /* 停止位 */ uint32_t Parity; /* 奇偶校驗位 */ uint32_t Mode; /* 傳送模式和接收模式使能 */ uint32_t HwFlowCtl; /* 硬體流控制 */ uint32_t OverSampling; /* 過取樣,可以選擇8倍和16倍過取樣 */ } UART_InitTypeDef;
配置串列埠引數,其實就是配置結構體UART_HandleTypeDef的成員。比如下面配置為波特率115200,8個數據位,無奇偶校驗,1個停止位。
UART_HandleTypeDef UartHandle; /* USART3工作在UART模式 */ /* 配置如下: - 資料位 = 8 Bits - 停止位 = 1 bit - 奇偶校驗位 = 無 - 波特率 = 115200bsp - 硬體流控制 (RTS 和 CTS 訊號) */ UartHandle.Instance = USART3; UartHandle.Init.BaudRate = 115200; UartHandle.Init.WordLength = UART_WORDLENGTH_8B; UartHandle.Init.StopBits = UART_STOPBITS_1; UartHandle.Init.Parity = UART_PARITY_NONE; UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; UartHandle.Init.Mode = UART_MODE_TX_RX; UartHandle.Init.OverSampling = UART_OVERSAMPLING_16; if(HAL_UART_Init(&UartHandle) != HAL_OK) { Error_Handler(); }
- 條件編譯USE_HAL_UART_REGISTER_CALLBACKS
用於串列埠回撥函式的設定。
如果要用到這種回撥函式定義方式,可以在stm32f4xx_hal_conf.h檔案裡面使能。
23.3.3 串列埠的底層配置(GPIO、時鐘、中斷等)
串列埠外設的基本引數配置完畢後還不能使用,還需要配置GPIO、時鐘、中斷等引數,比如下面配置串列埠1,使用引腳PA9和PA10。
/* 串列埠1的GPIO PA9, PA10 */ #define USART1_CLK_ENABLE() __HAL_RCC_USART1_CLK_ENABLE() #define USART1_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() #define USART1_TX_GPIO_PORT GPIOA #define USART1_TX_PIN GPIO_PIN_9 #define USART1_TX_AF GPIO_AF7_USART1 /* ********************************************************************************************************* * 函 數 名: InitHardUart * 功能說明: 配置串列埠的硬體引數和底層 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void InitHardUart(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_PeriphCLKInitTypeDef RCC_PeriphClkInit; #if UART1_FIFO_EN == 1 /* 串列埠1 */ /* 使能 GPIO TX/RX 時鐘 */ USART1_TX_GPIO_CLK_ENABLE(); USART1_RX_GPIO_CLK_ENABLE(); /* 使能 USARTx 時鐘 */ USART1_CLK_ENABLE(); /* 配置TX引腳 */ GPIO_InitStruct.Pin = USART1_TX_PIN; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = USART1_TX_AF; HAL_GPIO_Init(USART1_TX_GPIO_PORT, &GPIO_InitStruct); /* 配置RX引腳 */ GPIO_InitStruct.Pin = USART1_RX_PIN; GPIO_InitStruct.Alternate = USART1_RX_AF; HAL_GPIO_Init(USART1_RX_GPIO_PORT, &GPIO_InitStruct); /* 配置NVIC the NVIC for UART */ HAL_NVIC_SetPriority(USART1_IRQn, 0, 1); HAL_NVIC_EnableIRQ(USART1_IRQn); /* 配置波特率、奇偶校驗 */ bsp_SetUartParam(USART1, UART1_BAUD, UART_PARITY_NONE, UART_MODE_TX_RX); CLEAR_BIT(USART1->SR, USART_SR_TC); /* 清除TC傳送完成標誌 */ CLEAR_BIT(USART1->SR, USART_SR_RXNE); /* 清除RXNE接收標誌 */ // USART_CR1_PEIE | USART_CR1_RXNEIE SET_BIT(USART1->CR1, USART_CR1_RXNEIE); /* 使能PE. RX接受中斷 */ #endif }
總結下來就是以下幾點:
- 配置GPIO引腳時鐘。
- 配置USART時鐘。
- 配置USART的傳送和接收引腳。
- 通過NVIC配置中斷。
- 配置波特率,奇偶校驗等,在上一小節有講。
- 清除TC和RXNE標誌,使能接收中斷。
關於這個底層配置有以下幾點要著重說明下:
- 串列埠傳送和接收引腳的複用模式選擇已經被HAL庫定義好,放在了stm32f4xx_hal_gpio_ex.h檔案裡面。比如串列埠1有兩個複用
#define GPIO_AF7_USART1 ((uint8_t)0x07) /* USART1 Alternate Function mapping */
為什麼使用的AF7,這個是在資料手冊裡定義好的:
那麼使用GPIO_AF7_USART1即可。
- 根據情況要清除TC傳送完成標誌和RXNE接收資料標誌,因為這兩個標誌位在使能了串列埠後就已經置位,所以當用戶使用了TC或者RX中斷後,就會進入一次中斷服務程式,這點要特別注意。
- HAL庫有個自己的底層初始化回撥函式HAL_UART_MspInit,是弱定義的,使用者可以在其它的C檔案裡面實現,並將相對的底層初始化在裡面實現。當用戶呼叫HAL_UART_Init後,會在此函式裡面呼叫HAL_UART_MspInit,對應的底層復位函式HAL_UART_MspDeInit是在函式HAL_UART_DeInit裡面被呼叫的。當然,使用者也可以自己初始化,不限制必須在兩個函式裡面實現。
- 上面舉的例子裡面沒有用到DMA,如果用到了DMA,也是要初始化的。
23.3.4 串列埠的狀態標誌清除問題
注,經常會有網友諮詢為什麼串列埠中斷服務程式裡面沒有做清除標誌。
下面我們介紹__HAL_USART_GET_FLAG函式。這個函式用來檢查USART標誌位是否被設定。
/** @brief Check whether the specified USART flag is set or not. * @param __HANDLE__: specifies the USART Handle * @param __FLAG__: specifies the flag to check. * This parameter can be one of the following values: * @arg USART_FLAG_TXFT: TXFIFO threshold flag * @arg USART_FLAG_RXFT: RXFIFO threshold flag * @arg USART_FLAG_RXFF: RXFIFO Full flag * @arg USART_FLAG_TXFE: TXFIFO Empty flag * @arg USART_FLAG_REACK: Receive enable ackowledge flag * @arg USART_FLAG_TEACK: Transmit enable ackowledge flag * @arg USART_FLAG_BUSY: Busy flag * @arg USART_FLAG_TXE: Transmit data register empty flag * @arg USART_FLAG_TC: Transmission Complete flag * @arg USART_FLAG_RXNE: Receive data register not empty flag * @arg USART_FLAG_IDLE: Idle Line detection flag * @arg USART_FLAG_ORE: OverRun Error flag * @arg USART_FLAG_UDR: UnderRun Error flag * @arg USART_FLAG_NE: Noise Error flag * @arg USART_FLAG_FE: Framing Error flag * @arg USART_FLAG_PE: Parity Error flag * @retval The new state of __FLAG__ (TRUE or FALSE). */ #define __HAL_USART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->ISR & (__FLAG__)) == (__FLAG__))
USART_FLAG有如下幾種取值:
請大家重點關注上表中紅字部分,USART標誌是需要軟體主動清零的。清零有兩種方式:一種是呼叫__HAL_USART_CLEAR_FLAG函式,另一種是操作相關暫存器後自動清零。
/** @brief Clear the specified USART pending flag. * @param __HANDLE__: specifies the USART Handle. * @param __FLAG__: specifies the flag to check. * This parameter can be any combination of the following values: * @arg USART_FLAG_TXFT: TXFIFO threshold flag * @arg USART_FLAG_RXFT: RXFIFO threshold flag * @arg USART_FLAG_RXFF: RXFIFO Full flag * @arg USART_FLAG_TXFE: TXFIFO Empty flag * @arg USART_FLAG_REACK: Receive enable ackowledge flag * @arg USART_FLAG_TEACK: Transmit enable ackowledge flag * @arg USART_FLAG_WUF: Wake up from stop mode flag * @arg USART_FLAG_RWU: Receiver wake up flag (is the USART in mute mode) * @arg USART_FLAG_SBKF: Send Break flag * @arg USART_FLAG_CMF: Character match flag * @arg USART_FLAG_BUSY: Busy flag * @arg USART_FLAG_ABRF: Auto Baud rate detection flag * @arg USART_FLAG_ABRE: Auto Baud rate detection error flag * @arg USART_FLAG_RTOF: Receiver timeout flag * @arg USART_FLAG_LBD: LIN Break detection flag * @arg USART_FLAG_TXE: Transmit data register empty flag * @arg USART_FLAG_TC: Transmission Complete flag * @arg USART_FLAG_RXNE: Receive data register not empty flag * @arg USART_FLAG_IDLE: Idle Line detection flag * @arg USART_FLAG_ORE: OverRun Error flag * @arg USART_FLAG_NE: Noise Error flag * @arg USART_FLAG_FE: Framing Error flag * @arg USART_FLAG_PE: Parity Error flag * @retval The new state of __FLAG__ (TRUE or FALSE). */ #define __HAL_USART_CLEAR_FLAG(__HANDLE__, __FLAG__) ((__HANDLE__)->Instance->ICR = (__FLAG__))
上面介紹的USART標誌大部分能夠設定為產生中斷,也就是有對應的USART中斷標誌。我們只介紹幾個串列埠驅動要用到的中斷標誌:
USART_IT_TXE:TXE:傳送資料暫存器空(此時資料可能正在傳送)。
USART_IT_TC:傳送完成 。
USART_IT_RXNE:接收資料暫存器非空。
中斷預設都是關閉的,通過__HAL_USART_ENABLE_IT函式可以使能相應的中斷標誌。函式定義如下:
/** @brief Enable the specified USART interrupt. * @param __HANDLE__: specifies the USART Handle. * @param __INTERRUPT__: specifies the USART interrupt source to enable. * This parameter can be one of the following values: * @arg USART_IT_RXFF: RXFIFO Full interrupt * @arg USART_IT_TXFE: TXFIFO Empty interrupt * @arg USART_IT_RXFT: RXFIFO threshold interrupt * @arg USART_IT_TXFT: TXFIFO threshold interrupt * @arg USART_IT_TXE : Transmit Data Register empty interrupt * @arg USART_IT_TC : Transmission complete interrupt * @arg USART_IT_RXNE: Receive Data register not empty interrupt * @arg USART_IT_IDLE: Idle line detection interrupt * @arg USART_IT_PE : Parity Error interrupt * @arg USART_IT_ERR : Error interrupt(Frame error, noise error, overrun error) * @retval None */ #define __HAL_USART_ENABLE_IT(__HANDLE__, __INTERRUPT__) (((((uint8_t)(__INTERRUPT__)) >> 5U) == 1)? ((__HANDLE__)->Instance->CR1 |= (1U << ((__INTERRUPT__) & USART_IT_MASK))): \ ((((uint8_t)(__INTERRUPT__)) >> 5U) == 2)? ((__HANDLE__)->Instance->CR2 |= (1U << ((__INTERRUPT__) & USART_IT_MASK))): \ ((__HANDLE__)->Instance->CR3 |= (1U << ((__INTERRUPT__) & USART_IT_MASK))))
STM32一個串列埠的中斷服務程式入口地址只有一個,進入中斷服務程式後,我們需要判斷是什麼原因進入的中斷,因此需要呼叫一個函式來檢測中斷標誌。函式原型如下:
#define __HAL_USART_GET_IT(__HANDLE__, __IT__) ((__HANDLE__)->Instance->ISR & ((uint32_t)1 << ((__IT__)>> 0x08)))
中斷處理完畢後,必須軟體清除中斷標誌,否則中斷返回後,會重入中斷。清中斷標誌位的函式為:
#define __HAL_USART_CLEAR_IT(__HANDLE__, __IT_CLEAR__) ((__HANDLE__)->Instance->ICR = (uint32_t)(__IT_CLEAR__))
正如前面介紹的,不是所有的標誌都需要用這個函式清零。
注意:操作串列埠的暫存器不限制必須要用HAL庫提供的API,比如要操作暫存器CR1,直接呼叫USART1->CR1操作即可。
23.3.5 串列埠初始化流程總結
使用方法由HAL庫提供:
第1步:定義UART_HandleTypeDef型別串列埠結構體變數,比如UART_HandleTypeDef huart。
第2步:使用函式HAL_UART_MspInit初始化串列埠底層,不限制一定要用此函式裡面初始化,使用者也可以自己實現。
- 使能串列埠時鐘。
- 引腳配置。
a、使能串列埠所使用的GPIO時鐘。
b、配置GPIO的複用模式。
- 如果使用中斷方式函式HAL_UART_Transmit_IT和HAL_UART_Receive_IT需要做如下配置。
a、配置串列埠中斷優先順序。
b、使能串列埠中斷。
- 串列埠中斷的開關是通過函式__HAL_UART_ENABLE_IT() 和 __HAL_UART_DISABLE_IT()來實現,這兩個函式被巢狀到串列埠的傳送和接收函式中呼叫。
- 如果使用DMA方式函式HAL_UART_Transmit_DMA和HAL_UART_Receive_DMA需要做如下配置。
a、宣告串列埠的傳送和接收DMA結構體變數,注意傳送和接收是獨立的,如果都使用,那就都需要配置。
b、使能DMA介面時鐘。
c、配置串列埠的傳送和接收DMA結構體變數。
d、配置DMA傳送和接收通道。
e、關聯DMA和串列埠的控制代碼。
f、配置傳送DMA和接收DMA的傳輸完成中斷和中斷優先順序。
第3步:配置串列埠的波特率,位長,停止位,奇偶校驗位,流控制和傳送接收模式。
第4步:串列埠初始化呼叫的函式HAL_UART_Init初始化。
23.4 原始檔stm32f4xx_hal_uart.c
此檔案涉及到的函式較多,這裡把幾個常用的函式做個說明:
- HAL_UART_Init
- HAL_UART_Transmit
- HAL_UART_Receive
- HAL_UART_Transmit_IT
- HAL_UART_Receive_IT
- HAL_UART_Transmit_DMA
- HAL_UART_Receive_DMA
其實V5開發板設計的6個串列埠FIFO驅動檔案bsp_uart_fifo.c僅用到了函式HAL_UART_Init,其它函式都沒有用到,不過這裡也為大家做個說明。
23.4.1 函式HAL_UART_Init
函式原型:
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart) { /* 省略 */ if(huart->gState == HAL_UART_STATE_RESET) { huart->Lock = HAL_UNLOCKED; /* 初始化硬體: GPIO, CLOCK */ HAL_UART_MspInit(huart); } huart->gState = HAL_UART_STATE_BUSY; /* 禁止串列埠 */ __HAL_UART_DISABLE(huart); /* 配置串列埠引數 */ if (UART_SetConfig(huart) == HAL_ERROR) { return HAL_ERROR; } /* 配置串列埠高階特性 */ if (huart->AdvancedInit.AdvFeatureInit != UART_ADVFEATURE_NO_INIT) { UART_AdvFeatureConfig(huart); } /* 清暫存器的一些標誌位 */ CLEAR_BIT(huart->Instance->CR2, (USART_CR2_LINEN | USART_CR2_CLKEN)); CLEAR_BIT(huart->Instance->CR3, (USART_CR3_SCEN | USART_CR3_HDSEL | USART_CR3_IREN)); /* 使能串列埠 */ __HAL_UART_ENABLE(huart); return (UART_CheckIdleState(huart)); }
函式描述:
此函式用於初始化串列埠的基礎特性和高階特性。
函式引數:
- 第1個引數是UART_HandleTypeDef型別結構體指標變數,用於配置要初始化的引數。
- 返回值,返回HAL_TIMEOUT表示超時,HAL_ERROR表示引數錯誤,HAL_OK表示傳送成功,HAL_BUSY表示串列埠忙,正在使用中。
注意事項:
- 函式HAL_UART_MspInit用於初始化USART的底層時鐘、引腳等功能。需要使用者自己在此函式裡面實現具體的功能。由於這個函式是弱定義的,允許使用者在工程其它原始檔裡面重新實現此函式。當然,不限制一定要在此函式裡面實現,也可以像早期的標準庫那樣,使用者自己初始化即可,更靈活些。
- 如果形參huart的結構體成員gState沒有做初始狀態,這個地方就是個坑。特別是使用者搞了一個區域性變數UART_HandleTypeDef UartHandle。
對於區域性變數來說,這個引數就是一個隨機值,如果是全域性變數還好,一般MDK和IAR都會將全部變數初始化為0,而恰好這個 HAL_UART_STATE_RESET = 0x00U。
解決辦法有三
方法1:使用者自己初始串列埠和涉及到的GPIO等。
方法2:定義UART_HandleTypeDef UartHandle為全域性變數。
方法3:下面的方法
if(HAL_UART_DeInit(&UartHandle) != HAL_OK) { Error_Handler(); } if(HAL_UART_Init(&UartHandle) != HAL_OK) { Error_Handler(); }
- 注意串列埠的中斷狀態暫存器USART_ISR復位後,TC傳送完成狀態和RXNE接收狀態都被置1,如果使用者使能這兩個中斷前,最好優先清除中斷標誌。
使用舉例:
UART_HandleTypeDef UartHandle; /* USART3工作在UART模式 */ /* 配置如下: - 資料位 = 8 Bits - 停止位 = 1 bit - 奇偶校驗位 = 無 - 波特率 = 115200bsp - 硬體流控制 (RTS 和 CTS 訊號) */ UartHandle.Instance = USART3; UartHandle.Init.BaudRate = 115200; UartHandle.Init.WordLength = UART_WORDLENGTH_8B; UartHandle.Init.StopBits = UART_STOPBITS_1; UartHandle.Init.Parity = UART_PARITY_NONE; UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; UartHandle.Init.Mode = UART_MODE_TX_RX; UartHandle.Init.OverSampling = UART_OVERSAMPLING_16; if(HAL_UART_Init(&UartHandle) != HAL_OK) { Error_Handler(); }
23.4.2 函式HAL_UART_Transmit
函式原型:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) { /* 省略 */ if(huart->gState == HAL_UART_STATE_READY) { /* 省略 */ /* 上鎖 */ __HAL_LOCK(huart); while(huart->TxXferCount > 0U) { huart->TxXferCount--; /資料是9bit */ if (huart->Init.WordLength == UART_WORDLENGTH_9B) { /* 等待發送空標誌 */ if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } tmp = (uint16_t *) pData; huart->Instance->DR = (*tmp & (uint16_t)0x01FF); if (huart->Init.Parity == UART_PARITY_NONE) { pData += 2U; } else { pData += 1U; } } /* 其它資料寬度 */ else { /* 等待發送空標誌 */ if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } huart->Instance->DR = (*pData++ & (uint8_t)0xFF); } } /* 等待發送完成 */ if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } /* 就緒 */ huart->gState = HAL_UART_STATE_READY; /* 解鎖 */ __HAL_UNLOCK(huart); return HAL_OK; } else { return HAL_BUSY; } }
函式描述:
此函式以查詢的方式傳送指定位元組。看原始碼的話,程式裡面最重要的就是上面程式碼中置紅的兩個標誌,傳送空標誌和傳送完成標誌。傳送空標誌表示傳送資料暫存器為空,資料還在移位暫存器裡面,而傳送完成標誌表示資料已經從移位暫存器傳送出去。
函式引數:
- 第1個引數是UART_HandleTypeDef型別結構體指標變數。
- 第2個引數是要傳送的資料地址。
- 第3個引數是要傳送的資料大小,單位位元組。
- 第4個引數是溢位時間,單位ms。
- 返回值,返回HAL_TIMEOUT表示超時,HAL_ERROR表示引數錯誤,HAL_OK表示傳送成功,HAL_BUSY表示串列埠忙,正在使用中。
使用舉例:
/* ********************************************************************************************************* * 函 數 名: fputc * 功能說明: 重定義putc函式,這樣可以使用printf函式從串列埠1列印輸出 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ int fputc(int ch, FILE *f) { HAL_UART_Transmit(&UartHandle, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; }
23.4.3 函式HAL_UART_Receive
函式原型:
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) { uint16_t *tmp; uint32_t tickstart = 0U; /* 檢測RX接收狀態就緒 */ if (huart->RxState == HAL_UART_STATE_READY) { if ((pData == NULL) || (Size == 0U)) { return HAL_ERROR; } /* 上鎖 */ __HAL_LOCK(huart); huart->ErrorCode = HAL_UART_ERROR_NONE; huart->RxState = HAL_UART_STATE_BUSY_RX; /* 獲取當前的計數 */ tickstart = HAL_GetTick(); huart->RxXferSize = Size; huart->RxXferCount = Size; /* 阻塞式接收資料 */ while (huart->RxXferCount > 0U) { huart->RxXferCount--; /* 接收的資料是9bit的 */ if (huart->Init.WordLength == UART_WORDLENGTH_9B) { if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } tmp = (uint16_t *) pData; if (huart->Init.Parity == UART_PARITY_NONE) { *tmp = (uint16_t)(huart->Instance->DR & (uint16_t)0x01FF); pData += 2U; } else { *tmp = (uint16_t)(huart->Instance->DR & (uint16_t)0x00FF); pData += 1U; } } /* 其它bit接收 */ else { if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } if (huart->Init.Parity == UART_PARITY_NONE) { *pData++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF); } else { *pData++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F); } } } /* 接收就緒 At end of Rx process, restore huart->RxState to Ready */ huart->RxState = HAL_UART_STATE_READY; /* 解鎖 */ __HAL_UNLOCK(huart); return HAL_OK; } else { return HAL_BUSY; } }
函式描述:
此函式以查詢的方式接收指定位元組。這個函式相對比較好理解,就是等待上面程式中的RXNE標誌,置位了表示接收資料暫存器已經存入資料。
函式引數:
- 第1個引數是UART_HandleTypeDef型別結構體指標變數。
- 第2個引數是要接收的資料地址。
- 第3個引數是要接收的資料大小,單位位元組。
- 第4個引數是溢位時間,單位ms。
- 返回值,返回HAL_TIMEOUT表示超時,HAL_ERROR表示引數錯誤,HAL_OK表示傳送成功,HAL_BUSY表示串列埠忙,正在使用中。
使用舉例:
/* ********************************************************************************************************* * 函 數 名: fgetc * 功能說明: 重定義getc函式,這樣可以使用scanff函式從串列埠1輸入資料 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ int fgetc(FILE *f) { int ret; HAL_UART_Receive(&UartHandle, (uint8_t *)&ret, 1, HAL_MAX_DELAY); return ret; }
23.4.4 函式HAL_UART_Transmit_IT
函式原型:
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { /* 檢測是否處於就緒狀態 */ if (huart->gState == HAL_UART_STATE_READY) { if ((pData == NULL) || (Size == 0U)) { return HAL_ERROR; } /* 上鎖 */ __HAL_LOCK(huart); huart->pTxBuffPtr = pData; huart->TxXferSize = Size; huart->TxXferCount = Size; huart->ErrorCode = HAL_UART_ERROR_NONE; huart->gState = HAL_UART_STATE_BUSY_TX; /* 解鎖 */ __HAL_UNLOCK(huart); /* 使能傳送空中斷 */ __HAL_UART_ENABLE_IT(huart, UART_IT_TXE); return HAL_OK; } else { return HAL_BUSY; } }
函式描述:
此函式以中斷的方式傳送指定位元組,可以選擇使能FIFO中斷方式或者傳送空中斷方式。具體資料的傳送是在中斷處理函式HAL_UART_IRQHandler裡面實現。
函式引數:
- 第1個引數是UART_HandleTypeDef型別結構體指標變數。
- 第2個引數是要傳送的資料地址。
- 第3個引數是要傳送的資料大小,單位位元組。
- 返回值,返回HAL_ERROR表示引數錯誤,HAL_OK表示傳送成功,HAL_BUSY表示串列埠忙,正在使用中。
使用舉例:
使用中斷方式要使能串列埠中斷,此貼有完整例子:
http://www.armbbs.cn/forum.php?mod=viewthread&tid=86245 。
UART_HandleTypeDef UartHandle; uint8_t s_ucBuf[5]; /* 資料傳送 */ HAL_UART_Transmit_IT(&UartHandle, s_ucBuf, 1); HAL_UART_Transmit_IT(&UartHandle, (uint8_t*)"KEY_DOWN_K1\r\n", 13);
23.4.5 函式HAL_UART_Receive_IT
函式原型:
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { /* 檢查是否處於就緒態 */ if (huart->RxState == HAL_UART_STATE_READY) { if ((pData == NULL) || (Size == 0U)) { return HAL_ERROR; } /* 上鎖 */ __HAL_LOCK(huart); huart->pRxBuffPtr = pData; huart->RxXferSize = Size; huart->RxXferCount = Size; huart->ErrorCode = HAL_UART_ERROR_NONE; huart->RxState = HAL_UART_STATE_BUSY_RX; /* 解鎖 */ __HAL_UNLOCK(huart); /* 使能校驗錯誤中斷 */ __HAL_UART_ENABLE_IT(huart, UART_IT_PE); /* 使能幀錯誤,噪聲錯誤和溢位中斷 */ __HAL_UART_ENABLE_IT(huart, UART_IT_ERR); /* 使能接收非空中斷 ,即接收中斷 / __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); return HAL_OK; } else { return HAL_BUSY; } }
函式描述:
此函式以中斷的方式接收指定位元組,使能了奇偶校驗中斷失敗和錯誤中斷。具體資料的接收是在中斷處理函式HAL_UART_IRQHandler裡面實現。
函式引數:
- 第1個引數是UART_HandleTypeDef型別結構體指標變數。
- 第2個引數是要接收的資料地址。
- 第3個引數是要接收的資料大小,單位位元組。
- 返回值,返回HAL_ERROR表示引數錯誤,HAL_OK表示傳送成功,HAL_BUSY表示串列埠忙,正在使用中。
使用舉例:
使用中斷方式要使能串列埠中斷,此貼有完整例子:
http://www.armbbs.cn/forum.php?mod=viewthread&tid=86245 。
UART_HandleTypeDef UartHandle; uint8_t s_ucBuf[5]; /* 資料接收*/ HAL_UART_Receive_IT(&UartHandle, s_ucBuf, 1);
23.4.6 函式HAL_UART_Transmit_DMA
函式原型:
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { uint32_t *tmp; /* 檢查是否處於就緒態 */ if (huart->gState == HAL_UART_STATE_READY) { if ((pData == NULL) || (Size == 0U)) { return HAL_ERROR; } /* 上鎖 */ __HAL_LOCK(huart); huart->pTxBuffPtr = pData; huart->TxXferSize = Size; huart->TxXferCount = Size; huart->ErrorCode = HAL_UART_ERROR_NONE; huart->gState = HAL_UART_STATE_BUSY_TX; /* 設定串列埠DMA傳輸完成回撥 */ huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt; /* 設定串列埠傳輸完成回撥 */ huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt; /* 設定DMA錯誤回撥 */ huart->hdmatx->XferErrorCallback = UART_DMAError; /* 設定DMA終止回撥 */ huart->hdmatx->XferAbortCallback = NULL; /* 使能串列埠DMA方式傳送 */ tmp = (uint32_t *)&pData; HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size); /* 清除TC標誌 */ __HAL_UART_CLEAR_FLAG(huart, UART_FLAG_TC); /* 解鎖 */ __HAL_UNLOCK(huart); /* 使能串列埠DMA */ SET_BIT(huart->Instance->CR3, USART_CR3_DMAT); return HAL_OK; } else { return HAL_BUSY; } }
函式描述:
此函式以DMA的方式傳送指定位元組。這裡是用的DMA中斷方式HAL_DMA_Start_IT進行的傳送。所以使用此函式的話,不要忘了寫DMA中斷服務程式。而且DMA的配置也是需要使用者實現的,可以直接在函式HAL_UART_MspInit裡面實現,也可以放在其它位置。
函式引數:
- 第1個引數是UART_HandleTypeDef型別結構體指標變數。
- 第2個引數是要傳送的資料地址。
- 第3個引數是要傳送的資料大小,單位位元組。
- 返回值,返回HAL_ERROR表示引數錯誤,HAL_OK表示傳送成功,HAL_BUSY表示串列埠忙,正在使用中。
使用舉例:
使用DMA方式,此貼有完整例子:
http://www.armbbs.cn/forum.php?mod=viewthread&tid=86271 。
23.4.7 函式HAL_UART_Receive_DMA
函式原型:
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { uint32_t *tmp; /* 檢查是否處於就緒態 */ if (huart->RxState == HAL_UART_STATE_READY) { if ((pData == NULL) || (Size == 0U)) { return HAL_ERROR; } /* 上鎖 */ __HAL_LOCK(huart); huart->pRxBuffPtr = pData; huart->RxXferSize = Size; huart->ErrorCode = HAL_UART_ERROR_NONE; huart->RxState = HAL_UART_STATE_BUSY_RX; /* 設定串列埠DMA接收完成回撥函式 */ huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt; /* 設定串列埠半傳輸完成回撥函式 */ huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt; /* 設定DMA錯誤回撥 */ huart->hdmarx->XferErrorCallback = UART_DMAError; /* 設定DMA終止傳輸回撥 */ huart->hdmarx->XferAbortCallback = NULL; /* 使能串列埠DMA Enable the DMA stream */ tmp = (uint32_t *)&pData; HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size); /*在啟用DMA Rx請求之前清除溢位標誌:可以強制第2次傳輸 */ __HAL_UART_CLEAR_OREFLAG(huart); /* 上鎖 */ __HAL_UNLOCK(huart); /* 使能校驗錯誤中斷 */ SET_BIT(huart->Instance->CR1, USART_CR1_PEIE); /* 使能幀錯誤,噪聲錯誤和溢位中斷 */ SET_BIT(huart->Instance->CR3, USART_CR3_EIE); /* 使能串列埠DMA */ SET_BIT(huart->Instance->CR3, USART_CR3_DMAR); return HAL_OK; } else { return HAL_BUSY; } }
函式描述:
此函式以DMA的方式接收指定位元組。這裡是用的DMA中斷方式HAL_DMA_Start_IT進行的接收。所以使用此函式的話,不要忘了寫DMA中斷服務程式。而且DMA的配置也是需要使用者實現的,可以直接在函式HAL_UART_MspInit裡面實現,也可以放在其它位置。
函式引數:
- 第1個引數是UART_HandleTypeDef型別結構體指標變數。
- 第2個引數是要接收的資料地址。
- 第3個引數是要接收的資料大小,單位位元組。
- 返回值,返回HAL_ERROR表示引數錯誤,HAL_OK表示傳送成功,HAL_BUSY表示串列埠忙,正在使用中。
使用舉例:
使用DMA方式,此貼有完整例子:
http://www.armbbs.cn/forum.php?mod=viewthread&tid=86271 。
23.5 總結
本章節就為大家講解這麼多,涉及到的知識點和API函式比較多,需要花點時間消化,後面用到的多了,就可以熟練掌握了。