概述
上一篇写了 STM32CubeMX 的串口的使用,而这篇来扒一扒,它是怎么进行封装的。
可以学到的知识
- STM32CubeMX 是怎么用结构体封装串口
- 如何用结构体直接访问寄存器
- stm32外设时钟是怎么看的
- stm32的内存长什么样子
- 常量指针如何使用
- ...
一.串口实例
我们都知道,其实单片机最后其实都是对 串口相关的寄存器 进行操作,那么我们想扒一扒它的流程,必然要先知道串口相关的寄存器是哪些,因此查阅 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在内存的地址
从上图可以看到 STM32F407 的内存映射图,内存被分成了很多块,而串口是属于外设,因此现在只要关心从下往上数第三块内存块就行了,但是目前我们也只能从中看到有 AHB1,AHB2,APB1,APB2
,用标准库的小伙伴,应该对这些词很熟悉了,就是经常要对其进行时钟初始化的外设总线。
但是我们还是没有看到我们想要的 地址,因此我们还是要借助一张时钟树的图,来判断串口1
结合时钟树和内存映射图,我们可以看到,串口1的地址在0x40010000
到 0x4001 4BFF
,但是还不是真正我们想要的
最终,你会在 STM32F4xx中文参考手册
这个地址范围就是我们要对其进行串口操作的实际地址,但是我们的操作实际就用到了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的基地址为:
c 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
)就是发送失败了,返回超时。
c if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } //查询 UART_FLAG_TXE 标志位
- 如果没有超时,就通过下面代码进行发送,pData 指针 指向我们想要发送的数据的首地址
c 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
)就是接收失败了,返回超时。
c if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } //查询 UART_FLAG_RXNE 标志位
- 如果没有超时,就通过下面代码进行数据接收,pData 指针 指向我们想要缓存数据的首地址(一般为数组首地址),将 DR 中的数据读取出来。
c *pData++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
总结:
该函数就是一直在查询是否是接收状态,如果可以接收就接收,不能接收,就等到Timeout
结束,返回超时,值得注意的是
当接收超时时,计算接收到的个数 = huart1.RxXferSize - huart1.RxXferCount - 1
后续我还会继续分享STM32CuBeMX的相关操作,以及对库函数源码进行剖析,相信你会学到更多知识。