概述
上一篇寫了 STM32CubeMX 的串口的使用,而這篇來扒一扒,它是怎麼進行封裝的。其實在標準庫中也類似如下過程。
一.串口實例
我們都知道,其實單片機最後其實都是對 串口相關的寄存器 進行操作,那麼我們想扒一扒它的流程,必然要先知道串口相關的寄存器是哪些,因此查閱 STM32F4xx中文參考手冊 ,我們可以在 第711頁 找到以下相關寄存器:
- 狀態寄存器
- 數據寄存器
- 波特率寄存器
-
控制寄存器1
-
控制寄存器2
-
控制寄存器3
- 保護時間和預分頻器寄存器
這些就是操作串口所需要的寄存器,那麼我們應該怎麼和 HAL庫 的東西對應起來,去HAL庫中找找,我們就會在 stm32f407xx.h
的頭文件中找到以下結構體
/**
* @brief Universal Synchronous Asynchronous Receiver Transmitter
*/
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;
你會發現,寄存器和結構體中的變量名一一對應,那就是它了,我們再深入研究以下,爲什麼每個聲明都是用uint32_t
的類型?我們還可以在 STM32F4xx中文參考手冊 找到以下這張圖,更直觀的解釋原因
從上圖可以看出,每個寄存器都是之間的地址偏移都是 4個字節,也就是 32 bit ,所以是用 uint32_t
,使每個寄存器佔用 4 個字節,符合芯片文檔。
可是不知道你發現了沒有,這邊都沒有說串口在內存的實際地址,有些小夥伴可能也不知道,就算有地址要怎麼操作,這裏我們稍微複習一下C 語言中的 常量指針 的知識,下面給出一小段代碼
*(uint32_t *)0x40011000 = 0xFF;
這段代碼的含義,就是將 0x40011000
這個值轉爲指向一個32 bit 的內存空間的指針,* 取地址符號就是使得0xFF存入該 32 bit的空間內。
因此我們應該要找到我們所使用的 串口1在內存的地址 ,說了這麼多遍內存,我們就來看看 STM32 的內存到底長什麼樣子,我們可以在 對應的芯片手冊上找到其內存映射圖,當前芯片是 STM32F407,因此我們在芯片手冊中會看到
從上圖可以看到 STM32F407 的內存映射圖,內存被分成了很多塊,而串口是屬於外設,因此現在只要關心從下往上數第三塊內存塊就行了,但是目前我們也只能從中看到有 AHB1,AHB2,APB1,APB2
,用標準庫的小夥伴,應該對這些詞很熟悉了,就是經常要對其進行時鐘初始化的外設總線。
但是我們還是沒有看到我們想要的 地址,因此我們還是要藉助一張時鐘樹的圖,來判斷串口1 在哪一條總線之下。
結合時鐘樹和內存映射圖,我們可以看到,串口1的地址在0x40010000
到 0x4001 4BFF
,但是還不是真正我們想要的
最終,你會在 STM32F4xx中文參考手冊 中發現一張存儲的映射表,串口1的地址範圍就寫在那裏
這個地址範圍就是我們要對其進行串口操作的實際地址,但是我們的操作實際就用到了7個寄存器
爲了更形象的說明,我們用以下代碼,將前面提到的寄存器的實際的地址打印出來:
打印結果如下:
huart1.Instance->SR 40011000
huart1.Instance->DR 40011004
huart1.Instance->BRR 40011008
huart1.Instance->CR1 4001100c
huart1.Instance->CR2 40011010
huart1.Instance->CR3 40011014
huart1.Instance->CTPR 40011018
現在我們就很明白了,串口1的基地址 (40011000
)+ 上面所說的地址偏移量 = 每個寄存器的實際地址,
接下來我們再看看,這個基地址是怎麼在 HAL 庫中用上的。我們從上面的分析中知道,串口1其實是在外設總線上的,並且是 APB2
外設總線上,所以還是在stm32f407.h
的頭文件中找,我們可以找到以下宏定義:
- 外設基地址
- APB2基地址
- 串口1基地址
-
串口1用
USART_TypeDef
結構體封裝這樣我們就可以算出,串口1的基地址爲:
USART1_BASE = 0x40000000 + 0x00010000 + 0x1000 USART1_BASE = 0x40011000 //正如我們上述文檔所看到的一樣
將這個值通過
USART_TypeDef
結構體指針的強制轉換,就可以用結構體的方式來對寄存器進行存取值了(昇華一下下,其實數組的首地址也可以用結構體指針進行強制轉換,只要結果符合你的預期就可以了,比如這個技巧可以用在網絡傳輸的時候)
現在來看看 USART1 用在哪裏了,我們可以在 usart.c
找到以下代碼
我們可以看到它用在了串口1初始化的函數(MX_USART1_UART_Init
)裏面了,也終於看到了我們這個標題要說的實例( huart1
)了
接下來我們看看這個實例的結構體( UART_HandleTypeDef
)長什麼樣子
/**
* @brief UART handle Structure definition
*/
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;
__IO HAL_UART_StateTypeDef RxState;
__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 CompleCallback */
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 */
void (* MspInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp Init callback*/
void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp DeInitcallback*/
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
} UART_HandleTypeDef;
我們可以看到在這個結構體裏看到,它需要串口基地址,串口初始化所需要的值,以及數據接收和發送所需要的指針,和當前狀態標誌位。
二. 串口實例初始化
我們可以在串口初始化的函數(MX_USART1_UART_Init
)中,找到我們自己在STM32CubeMX
的圖形界面中爲串口配置的值,如下
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
這不是真正的初始化(因爲還沒操作到我們之前所說的寄存器),它只是用來記錄需要初始化的值
而真正的初始化這些參數,是在 HAL_UART_Init
中的 UART_SetConfig
函數中,如下代碼所示
/**
* @brief Configures the UART peripheral.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @retval None
*/
static void UART_SetConfig(UART_HandleTypeDef *huart)
{
uint32_t tmpreg;
uint32_t pclk;
/* Check the parameters */
assert_param(IS_UART_BAUDRATE(huart->Init.BaudRate));
assert_param(IS_UART_STOPBITS(huart->Init.StopBits));
assert_param(IS_UART_PARITY(huart->Init.Parity));
assert_param(IS_UART_MODE(huart->Init.Mode));
/*-------------------------- USART CR2 Configuration -----------------------*/
/* Configure the UART Stop Bits: Set STOP[13:12] bits
according to huart->Init.StopBits value */
MODIFY_REG(huart->Instance->CR2, USART_CR2_STOP, huart->Init.StopBits);
/*-------------------------- USART CR1 Configuration -----------------------*/
/* Configure the UART Word Length, Parity and mode:
Set the M bits according to huart->Init.WordLength value
Set PCE and PS bits according to huart->Init.Parity value
Set TE and RE bits according to huart->Init.Mode value
Set OVER8 bit according to huart->Init.OverSampling value */
tmpreg = (uint32_t)huart->Init.WordLength | huart->Init.Parity | huart->Init.Mode | huart->Init.OverSampling;
MODIFY_REG(huart->Instance->CR1,
(uint32_t)(USART_CR1_M | USART_CR1_PCE | USART_CR1_PS | USART_CR1_TE | USART_CR1_RE | USART_CR1_OVER8),
tmpreg);
/*-------------------------- USART CR3 Configuration -----------------------*/
/* Configure the UART HFC: Set CTSE and RTSE bits according to huart->Init.HwFlowCtl value */
MODIFY_REG(huart->Instance->CR3, (USART_CR3_RTSE | USART_CR3_CTSE), huart->Init.HwFlowCtl);
/* Check the Over Sampling */
if (huart->Init.OverSampling == UART_OVERSAMPLING_8)
{
/*-------------------------- USART BRR Configuration ---------------------*/
#if defined(USART6) && defined(UART9) && defined(UART10)
if ((huart->Instance == USART1) || (huart->Instance == USART6) || (huart->Instance == UART9) || (huart->Instance == UART10))
{
pclk = HAL_RCC_GetPCLK2Freq();
huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
}
#elif defined(USART6)
if ((huart->Instance == USART1) || (huart->Instance == USART6))
{
pclk = HAL_RCC_GetPCLK2Freq();
huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
}
#else
if (huart->Instance == USART1)
{
pclk = HAL_RCC_GetPCLK2Freq();
huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
}
#endif /* USART6 */
else
{
pclk = HAL_RCC_GetPCLK1Freq();
huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
}
}
else
{
/*-------------------------- USART BRR Configuration ---------------------*/
#if defined(USART6) && defined(UART9) && defined(UART10)
if ((huart->Instance == USART1) || (huart->Instance == USART6) || (huart->Instance == UART9) || (huart->Instance == UART10))
{
pclk = HAL_RCC_GetPCLK2Freq();
huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
}
#elif defined(USART6)
if ((huart->Instance == USART1) || (huart->Instance == USART6))
{
pclk = HAL_RCC_GetPCLK2Freq();
huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
}
#else
if (huart->Instance == USART1)
{
pclk = HAL_RCC_GetPCLK2Freq();
huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
}
#endif /* USART6 */
else
{
pclk = HAL_RCC_GetPCLK1Freq();
huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
}
}
}
/**
* @}
*/
它真正操作到我們所說那些串口相關的寄存器(CR1, CR2, CR3, BRR)的配置,這裏沒看到 (SR,DR),因爲它在數據的發送和接收時用到了。
三. 串口數據的發送(阻塞模式)
接下來就是對數據發送進行說明,先放出函數
/**
* @brief Sends an amount of data in blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the sent data is handled as a set of u16. In this case, Size must indicate the number
* of u16 provided through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be sent
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
uint16_t *tmp;
uint32_t tickstart = 0U;
/* Check that a Tx process is not already ongoing */
if (huart->gState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Process Locked */
__HAL_LOCK(huart);
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->gState = HAL_UART_STATE_BUSY_TX;
/* Init tickstart for timeout managment */
tickstart = HAL_GetTick();
huart->TxXferSize = Size;
huart->TxXferCount = Size;
/* Process Unlocked */
__HAL_UNLOCK(huart);
while (huart->TxXferCount > 0U)
{
huart->TxXferCount--;
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;
}
/* At end of Tx process, restore huart->gState to Ready */
huart->gState = HAL_UART_STATE_READY;
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
-
進來該函數 檢查當前串口的狀態,若處於準備狀態,將串口狀態設置爲
huart->gState = HAL_UART_STATE_BUSY_TX;
這個其實是爲了對串口做保護,防止同時使用串口,導致數據發送錯誤 -
huart->TxXferCount = Size
這變量記錄當前還需要 發送的數據個數,在while
中不斷進行判斷,如果需要發送數據就通過下面函數查詢 SR 寄存器中的狀態,超過指定時間(Timeout
)就是發送失敗了,返回超時。if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } //查詢 UART_FLAG_TXE 標誌位
-
如果沒有超時,就通過下面代碼進行發送,pData 指針 指向我們想要發送的數據的首地址
huart->Instance->DR = (*pData++ & (uint8_t)0xFF);
總結:
該函數就是一直在查詢是否是發送狀態,如果可以發送就發送,不能發送,就等到
Timeout
結束,返回超時
四. 串口數據接收
接下來就是對數據接收進行說明,再放出函數
/**
* @brief Receives an amount of data in blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the received data is handled as a set of u16. In this case, Size must indicate the number
* of u16 available through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be received.
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
uint16_t *tmp;
uint32_t tickstart = 0U;
/* Check that a Rx process is not already ongoing */
if (huart->RxState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Process Locked */
__HAL_LOCK(huart);
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->RxState = HAL_UART_STATE_BUSY_RX;
/* Init tickstart for timeout managment */
tickstart = HAL_GetTick();
huart->RxXferSize = Size;
huart->RxXferCount = Size;
/* Process Unlocked */
__HAL_UNLOCK(huart);
/* Check the remain data to be received */
while (huart->RxXferCount > 0U)
{
huart->RxXferCount--;
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;
}
}
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;
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
-
進來該函數 檢查當前串口的狀態,若處於準備狀態,將串口狀態設置爲
huart->gState = HAL_UART_STATE_BUSY_RX;
這也是防止同時使用串口,導致數據接收錯誤 -
huart->TxXferCount = Size
這變量記錄當前還需要 接收的數據個數,在while
中不斷進行判斷,如果需要接收數據就通過下面函數查詢 SR 寄存器中的狀態,超過指定時間(Timeout
)就是接收失敗了,返回超時。if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } //查詢 UART_FLAG_RXNE 標誌位
-
如果沒有超時,就通過下面代碼進行數據接收,pData 指針 指向我們想要緩存數據的首地址(一般爲數組首地址),將 DR 中的數據讀取出來。
*pData++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
總結:
該函數就是一直在查詢是否是接收狀態,如果可以接收就接收,不能接收,就等到
Timeout
結束,返回超時,值得注意的是 這裏如果沒有接收到足夠的數據也會返回超時,比如你要接收10個,如果只接收到了 9個 它也會返回超時,可以通過計算獲取到底接收到幾個。當接收超時時,計算接收到的個數 = huart1.RxXferSize - huart1.RxXferCount - 1
全文總結:
- 你會知道如何通過結構體去操作寄存器
- 你會知道 HAL 庫如何封裝串口
- 對串口寄存器有整體的瞭解
- …等等知識
後續還會繼續分享串口的其他基礎知識和使用,感興趣的小夥伴記得關注我!
-----------------------------------------------結束--------------------------------------------------------
文章有價值,請各位看官點個贊,關注我或者點右邊打個賞吧!