UART串口协议

本章节,将是UART章节的重中之重。我将详细讲述UART串口通信协议,并且会结合Robomaster官方提供的串口代码进行讲述。希望能够深入浅出地讲透UART串口协议。

UART概述

UART(Universal Asynchronous Receiver/Transmitter),即通用异步收发器。其中,异步体现在只需要拉低信号就可以开始传输数据。而这也是串口协议的一个关键特征,它不需要引入时钟信号来进行操作。收发器意味着在数字IC设计中,需要设计接收器和发送器。

UART帧格式

起始位 + 数据位 + 校验位 + 停止位

image.png

起始位

起始位只有一位,是”0“。当信号被拉低,就意味着一个信号需要开始被接收了。这就是UART通信异步的体现。在接收方空闲的时候,当检测到有一个低电平,则开始逐位接收数据。

在设计UART的时候,如何检测低电平,使用了电平检测电路。具体参阅 电控 | 数电理论基础——边缘检测电路

数据位

数据位可以是5位,6位,7位,或者8位。这取决于你所要发送的数据的多少。

这就有一个问题,为什么UART的数据位是可变的呢?

由于UART是一个低速总线,每多发一位都会占用不少时间。因此,可以根据传输数据的特点,采用不同的位宽来节约传输数据的时间。

校验位

校验采用的是奇偶校验的方式。奇偶校验可以存在也可以不存在。

想要知道奇偶校验是什么,如何实现的,具体参阅 电控 | 通信协议基本方略——校验

而奇偶校验需要如何电路实现,请参阅 电控 | 数电理论基础——奇偶校验

停止位

数据会在数据位或校验位的后端发送一个1位到2位的逻辑“1”电平,即高电平。当接收端接收到电平被拉高,UART总线就会进入空闲阶段。

代码实现

那么,UART协议如何通过代码来实现呢?

在我们通过CubeMX搭建好框架之后,UART协议就会自动生成了。这大大节约了嵌入式开发人员的工作量。

但是想要更加深入的了解它,就还是要学习它的代码。

UART初始化

CubeMX会帮我们根据在其上的设置配置好UART相关的参数。这些内容写在了初始化函数中。其中包括:句柄、波特率、停止位等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* USART1 init function */

void MX_USART1_UART_Init(void)
{

huart1.Instance = USART1;
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;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}

}

具体的解释如下。

我们在CubeMX中,可以看到如下的配置:

image.png

我们对应上代码,可以知道,在USART1的总线上,传输的是8位的信号。因此,数据为位8位(而不是其位数)。校验位设置为None,停止位设置为1。

串口发送函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* @brief Sends an amount of data in blocking mode.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer
* @param Size Amount of data 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;
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;

/* Process Unlocked */
__HAL_UNLOCK(huart);

return HAL_OK;
}
else
{
return HAL_BUSY;
}
}

这是串口的发送函数。他是在stm32f4xx_hal_uart.c中被定义的。因此,这是一个HAL库函数。

它非常的长,在这里,我们简单的来看一下。

首先,我们关注它的形参列表。这个函数需要传入这么几个东西。句柄数据变量数据大小超时时长

我们的具体在于协议,因此重点需要关注数据变量是如何被传入的。

数据想要被发送,需要做到的就是传入寄存器并被保存。

数据传入寄存器

实现这一步骤的语句如下:

1
2
3
4
5
6
7
8
else
{
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
{
return HAL_TIMEOUT;
}
huart->Instance->DR = (*pData++ & (uint8_t)0xFF);
}

hart:这是一个指向UART_HandleTypeDef 这个类型结构的指针,这个结构体包含了UART配置的所有信息

Instance:这是UART_HandleTypeDef这个类型结构的成员,指向一个特定的UART硬件寄存器组的基地址。

DR:代表 “Data Register”,是UART接口中用于数据传输的寄存器。写入这个寄存器的数据会被发送到UART总线上。

*pData 表示取 pData 指针当前指向的内存位置的值,即当前要发送的字节。

pData++ 是一个后置递增操作。这意味着在取得 *pData 的值之后,pData 指针会自动增加,指向下一个数据字节。这是处理发送缓冲区数据的常用方法,可以确保每次循环发送一个字节后,指针都会指向下一个待发送的字节。

& (uint8_t)0xFF 是一个位与操作,确保只有最低的8位被写入到数据寄存器。在这个特定的情况下,由于 pData 已经是一个指向 uint8_t 的指针,这个操作可能看起来多余。然而,这种操作可以提供额外的安全性,确保无论何种原因导致的高位数据污染都不会影响最终写入寄存器的值。

其他部分传入寄存器

其实这部分已经完成了,就在那个初始函数里面。

在初始函数中,我们看到了huart1这个指针。而在发送函数中,我们倘若使用它,也会传入huart1。因此,这些数据分成了两部分,共同整合在了同一个UART寄存器中,被发送出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* @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; /*!< 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); /*!< 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 Complete Callback */
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 DeInit callback */
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

} UART_HandleTypeDef;

那么寄存器是如何保存的呢?有一个专门的结构体给它。

1
2
3
4
5
6
7
8
9
10
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;

函数使用

函数的使用很简单。举Robomaster的官方例子:

1
2
3
4
5
6
7
8
9
10
11
12
while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
//send data by usart
//串口发送数据
HAL_UART_Transmit(&huart1, "RoboMaster\r\n", 12, 100);
HAL_Delay(100);
HAL_UART_Transmit(&huart6, "RoboMaster\r\n", 12, 100);
HAL_Delay(100);
}

串口接收函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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;

/* 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;

/* Process Unlocked */
__HAL_UNLOCK(huart);

return HAL_OK;
}
else
{
return HAL_BUSY;
}
}

我们可以发现,接收函数的传入参数和发送函数是一样的。但是,功能有所变化。它需要的是从寄存器中读取信息,并保存在变量中,因此,这个函数中最重要的部分如下:

从寄存器中读出数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
}

通过循环的方式,让pData移动,从而不断的讲DR中的数据保存到变量之中。

当没有校验位时,即UART_PARITY_NONE时,直接读取数据8位。

当有校验位时,则读取7位数据。也就是0-6位数据,因为第7位为校验位。

轮询方式

如果使用这种方式进行数据的发送和接收,被称为是串口的第一种通信方式:轮询。

具体的函数使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* USER CODE BEGIN 2 */
uint8_t receiveData[2];
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
HAL_UART_Receive(&huart1, receiveData, 2, HAL_MAX_DELAY);
HAL_UART_Transmit(&huart1, receiveData, 2, 100);
}
/* USER CODE END 3 */

如此这般,就可以在发送一段数据之后,电脑自动把这个数据接收到并显示(在串口工具上)。

但是,这种方式有很大的缺陷。它必须要阻塞主程序的运行,直到完成发送,或者等待超时。如果设置了无限等待,就会无限阻塞。在接收时,需要接收固定长度等等。

我们有更有利的工具来解决这个问题。请参阅下一个文章 电控 | 串口通信基础——串口中断与DMA