# 串口通讯

### 串口通讯

### 概念

#### 什么是串口通讯与并口通讯的区别？

> 1. 数据传输方式不同 串口通讯：串行传输，即一位一位地传输数据，数据传输速率较慢。 并口通讯：并行传输，即同时传输多个数据位，数据传输速率较快。
> 2. 数据线数量不同 串口通讯：只有两根数据线（TXD和RXD），因此连接简单，但速率较慢。 并口通讯：需要多根数据线（8根或16根），因此连接较为复杂，但速率较快。
> 3. 传输距离和数据速率限制不同 串口通讯：适用于短距离通讯，传输速率一般在115200bps以下。 并口通讯：适用于长距离通讯，传输速率可达数百Mbps。
> 4. 常用设备不同 串口通讯：串口通讯常用于单片机、调试工具、传感器等设备之间的通讯。 并口通讯：并口通讯常用于打印机、扫描仪、显示器等设备之间的通讯。 综上所述，串口通讯和并口通讯各有优缺点，应根据实际需要选择合适的通讯方式。串口通讯适用于短距离、低速率的通讯，而并口通讯适用于长距离、高速率的通讯。

#### UART就是串口通讯吗

> UART全称是通用异步收发器，是串口的一种，但不能说串口通讯就是UART，只是UART可以说是最常用的串口通讯方式。

#### UART与USART的区别

> 1. 通讯方式不同 UART：UART是异步通讯协议，使用一个引脚用于传输数据，即TX（发送）和RX（接收）引脚。 USART：USART可以同时支持异步和同步通讯，可以通过配置来选择使用异步或同步通讯。同步通讯使用时钟引脚，异步通讯使用TX和RX引脚。
> 2. 数据传输速率不同 UART：UART通常支持的速率较低，一般在1Mbps以下。 USART：USART支持的速率较高，可以达到数十Mbps。
> 3. 数据格式不同 UART：UART只支持固定的数据格式，如8个数据位、无校验位、1个停止位。 USART：USART可以通过配置支持不同的数据格式，如5-9个数据位、奇偶校验位、1-2个停止位等。
> 4. 功能不同 UART：UART只能支持基本的数据传输功能，不支持高级功能，如硬件流控制、多主机通讯等。 USART：USART支持多种高级功能，如硬件流控制、多主机通讯、自动波特率检测等。 综上所述，UART和USART虽然都是串口通讯协议，但是它们的通讯方式、数据传输速率、数据格式和功能都有所不同。在选择使用时需要根据实际需求选择合适的协议。

#### UART与RS485 RS232的关系

> **UART**，是通用异步收发传输器（Universal Asynchronous Receiver/Transmitter），既然是“器”，显然，它就是个设备而已，要完成一个特定的功能的硬件。
>
> &#x20; **RS232/RS485**，是两种不同的电气协议，也就是说，是对电气特性以及物理特性的规定，作用于数据的传输通路上，它并不内含对数据的处理方式。比如，最显著的特征是：RS232使用3－15v有效电平，而UART，因为对电气特性并没有规定，所以直接使用CPU使用的电平，就是所谓的TTL电平（可能在0\~3.3V之间）。更具体的，电气的特性也决定了线路的连接方式，比如RS232，规定用电平表示数据，因此线路就是单线路的，用两根线才能达到全双工的目的；而RS485， 使用差分电平表示数据，因此，必须用两根线才能达到传输数据的基本要求，要实现全双工，必需用4根线。但是，无论使用RS232还是RS485，它们与UART是相对独立的，但是由于电气特性的差别，必须要有专用的器件和UART接驳
>
> 从某种意义上，可以说，线路上存在的仅仅是电流，RS232/RS485规定了这些电流在什么样的线路上流动和流动的样式；在UART那里，电流才被解释和组装成数据，并变成CPU可直接读写的形式。
>
> 编程时候，无论是232还是485就是单纯的用UART进行通讯实验。232和485只是在硬件电路图上存在区别。
>
> 也可以从以下几个方面总结：
>
> 1. 信号电平不同 UART使用TTL电平，RS232使用正负12V的电平，而RS485使用差分信号，即两条信号线分别传输正负电平。
> 2. 通信距离不同 UART的通信距离较短，一般在几米范围内；RS232的通信距离可达15米左右；而RS485的通信距离可达1200米。
> 3. 传输速率不同 UART的传输速率一般较低，最高只能达到115200bps；RS232的传输速率可达115200bps；而RS485的传输速率可达10Mbps。
> 4. 支持设备数量不同 UART只支持点对点通信，即只能连接两个设备；RS232支持一对一或一对多的连接方式，可以连接多个设备；而RS485支持多对多的连接方式，可以连接多个设备。
> 5. 网络拓扑结构不同 UART没有特定的网络拓扑结构；RS232是点对点或星型拓扑结构；而RS485是总线型拓扑结构。

#### ModuBus

> 规定数据帧结构,是软件层面的协议，是一个应用层的协议，modbus可用于232和485，也就是说使用ModuBus的前提是完成UART的驱动

#### 常用的串行通讯协议

单工通信：数据只能沿一个方向传输

半双工通信：数据可以沿两个方向传输，但需要分时进行

全双工通信：数据可以同时进行双向传输

同步通信：共用同一时钟信号

异步通信：没有时钟信号，通过在数据信号中加入起始位和停止位等一些同步信号

| 通信接口           | 接口引脚                                           | 数据同步方式 | 数据传输方向 |
| -------------- | ---------------------------------------------- | ------ | ------ |
| UART (通用异步收发器) | TXD:发送端 RXD:接收端 GND:公共地                        | 异步通信   | 全双工    |
| 1-wire         | DQ:发送/接收端                                      | 异步通信   | 半双工    |
| IIC            | SCL:同步时钟 SDA:数据输入/输出端                          | 同步通信   | 半双工    |
| SPI            | SCK:同步时钟 MISO:主机输入，从机输出 MOSI:主机输出，从机输入 CS:片选信号 | 同步通信   | 全双工    |

#### 奇偶校验

要发送的字节是0x1a，二进制表示为0001 1010。

* 采用奇校验，则在数据后补上个0，数据变为0001 1010 0，数据中1的个数为奇数个（3个）
* 采用偶校验，则在数据后补上个1，数据变为0001 1010 1，数据中1的个数为偶数个（4个）

  > 接收方通过计算数据中1个数是否满足奇偶性来确定数据是否有错。
  >
  > 需要将WordLength选择为UART\_WORDLENGTH\_9B。因为使用奇偶校验时，数据帧的总长度会增加一位，因此需要将WordLength设置为9位，以保证正确的接收和发送

#### 停止位校验

停止位校验必须有，可以有0.5、1、1.5、2.0个位，保持逻辑1电平。但是在STM32的HAL库中似乎只能选择1个或者2个位

#### USART\_DR 寄存器

USART\_DR 串口数据（Data）寄存器；这是一个双寄存器，包含了TDR和RDR，对它读操作，读取的是RDR寄存器的值，对它的写操作，实际上是写到TDR寄存器的；当向该寄存器写数据的时候，串口就会自动发送，当收到收据的时候，也是存在该寄存器内。

#### 接收与传送过程数据框图

如下图，在这个过程我们其实操作的只有`USART_DR` 寄存器剩下的有硬件来完成

![](https://s2.loli.net/2023/04/17/3aH6IQSLM5nh8br.png)

> USART\_DR寄存器只有8位，因此1次只能接收8位，1个字节的数据，且接收导数据需要立即将其从DR寄存器读出，否则后续的数据会自动传至DR寄存器，覆盖原有数据，造成数据丢失，所以通常使用中断方式进行数据接收

### UART的驱动

#### 确定使用的串口号及对应的引脚

STM32F103ZET6共有3个USART,2个UART，这些数据可在选型手册获取，也可在对应芯片的数据手册的第一页获取，不过在USART的描述上似乎不太准确，这边直接说有5个USART显然是不准确的

![](https://s2.loli.net/2023/03/13/VSTfzy1KD9W6XLu.png)

查看参考手册8.3.3小节，可得到USART的复用功能重映射，下面以USART1为例

![](https://s2.loli.net/2023/03/13/R1aPECjcduS9oTW.png)

不过此处可能只有USART1-3的引脚，UART4,UART5没有，需要查看数据手册，手动搜索获得，值得注意的是在F103ZET6的数据手册的翻译版中由于翻译错误，将UART4,UART5写作USART4,USART5,因此需要搜索USART4,USART5，英文原版没有这个错误

![](https://s2.loli.net/2023/03/13/ugmP184AEQSFls5.png)

#### USART的中断类型

USART1==只有一个中断向量==，但是可以通过配置USART的寄存器来选择不同的中断，也就是说可以将多个事件都链接到这个中断向量上。可以查看参考手册25.4章节

![](https://s2.loli.net/2023/03/14/9irxLHf3PaZGltC.png)

在HAL库stm32f1xx\_hal\_uart.h进行了宏定义

```
#define UART_IT_PE                       ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_PEIE))
#define UART_IT_TXE                      ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_TXEIE))
#define UART_IT_TC                       ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_TCIE))
#define UART_IT_RXNE                     ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_RXNEIE))
#define UART_IT_IDLE                     ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_IDLEIE))

#define UART_IT_LBD                      ((uint32_t)(UART_CR2_REG_INDEX << 28U | USART_CR2_LBDIE))

#define UART_IT_CTS                      ((uint32_t)(UART_CR3_REG_INDEX << 28U | USART_CR3_CTSIE))
#define UART_IT_ERR                      ((uint32_t)(UART_CR3_REG_INDEX << 28U | USART_CR3_EIE))
```

#### USART的中断触发机制

具体的可以查看USART\_CR1寄存器，正如手册所描述的设置对应位为1，就会触发USART中断向量，调用服务函数

![](https://s2.loli.net/2023/03/14/BcMNkg9mQnXEHx1.png)

以接收中断为例，当USART\_SR中的RXNE为’1’时，产生USART中断

而USART\_SR是状态寄存器,描述如下

当RDR移位寄存器中的数据被转移到USART\_DR寄存器中，该位被硬件置位。如果 USART\_CR1寄存器中的RXNEIE为1，则产生中断。

![](https://s2.loli.net/2023/03/14/IYvnx7KBJ5jp4eu.png)

#### USART1的驱动与回调机制

**初始化与回调**

HAL\_UART\_Init()函数会调用HAL\_UART\_MspInit()

* 只得注意的是，这里有两个缓存区
* > 一个是给HAL库接收函数用的`g_rx_buffer`，缓存大小为1字节，只是为了在接收到数据后直接从DR寄存器将数据取出来存到这个缓存区
  >
  > 另一个是我们真正处理数据用的数据缓存`g_usart_rx_buf`，缓存大小一般大于1字节，会在`g_rx_buffer`中的数据合法的情况下将数据存入`g_usart_rx_buf`
  >
  > 之所以这样是因为，数据接收需要协议，HAL库的`UART_Receive_IT`函数只是单纯的将数据进行读取，不涉及到到数据协议，我们需要区分一次数据读取是否完成，就需要有数据帧的概念，即如何去判断一帧数据的完结，这就涉及到数据协议的概念，在回调函数`HAL_UART_RxCpltCallback`中就是根据数据协议的要求将数据存到`g_usart_rx_buf`中

```c
/* 接收缓冲, 最大USART_REC_LEN个字节. */
uint8_t g_usart_rx_buf[USART_REC_LEN];

/*  接收状态
 *  bit15，      接收完成标志
 *  bit14，      接收到0x0d
 *  bit13~0，    接收到的有效字节数目
*/
uint16_t g_usart_rx_sta = 0;

uint8_t g_rx_buffer[RXBUFFERSIZE];  /* HAL库使用的串口接收缓冲 */

UART_HandleTypeDef g_uart1_handle;  /* UART句柄 */

/**
 * @brief       串口X初始化函数
 * @param       baudrate: 波特率, 根据自己需要设置波特率值
 * @note        注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
 *              这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
 * @retval      无
 */
void usart_init(uint32_t baudrate)
{
    /*UART 初始化设置*/
    g_uart1_handle.Instance = USART_UX;                                       /* USART_UX */
    g_uart1_handle.Init.BaudRate = baudrate;                                  /* 波特率 */
    g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;                      /* 字长为8位数据格式 */
    g_uart1_handle.Init.StopBits = UART_STOPBITS_1;                           /* 一个停止位 */
    g_uart1_handle.Init.Parity = UART_PARITY_NONE;                            /* 无奇偶校验位 */
    g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;                      /* 无硬件流控 */
    g_uart1_handle.Init.Mode = UART_MODE_TX_RX;                               /* 收发模式 */
    HAL_UART_Init(&g_uart1_handle);                                           /* HAL_UART_Init()会使能UART1 */

    /* 该函数会开启接收中断：标志位UART_IT_RXNE，并且设置接收缓冲以及接收缓冲接收最大数据量 */
    HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE); 
}

/**
 * @brief       UART底层初始化函数
 * @param       huart: UART句柄类型指针
 * @note        此函数会被HAL_UART_Init()调用
 *              完成时钟使能，引脚配置，中断配置
 * @retval      无
 */
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef gpio_init_struct;

    if (huart->Instance == USART_UX)                            /* 如果是串口1，进行串口1 MSP初始化 */
    {
        USART_TX_GPIO_CLK_ENABLE();                             /* 使能串口TX脚时钟 */
        USART_RX_GPIO_CLK_ENABLE();                             /* 使能串口RX脚时钟 */
        USART_UX_CLK_ENABLE();                                  /* 使能串口时钟 */

        gpio_init_struct.Pin = USART_TX_GPIO_PIN;               /* 串口发送引脚号 */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;                /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* IO速度设置为高速 */
        HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct);
                
        gpio_init_struct.Pin = USART_RX_GPIO_PIN;               /* 串口RX脚 模式设置 */
        gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;    
        HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct);   /* 串口RX脚 必须设置成输入模式 */
        
#if USART_EN_RX
        HAL_NVIC_EnableIRQ(USART_UX_IRQn);                      /* 使能USART1中断通道 */
        HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3);              /* 组2，最低优先级:抢占优先级3，子优先级3 */
#endif
    }
}
```

* `HAL_UART_Init()`：该函数是针对应用程序层的，用于对UART外设进行配置和初始化。它会根据用户传入的参数对UART的各个寄存器进行配置，包括波特率、数据位、停止位、校验等参数。
* `HAL_UART_MspInit()`：该函数是针对底层的，用于初始化UART外设的底层硬件资源，如GPIO、DMA、NVIC等。它会对UART所需要的所有底层硬件资源进行初始化、配置和启用，以确保UART能够正常工作。

因此，`HAL_UART_Init()`和`HAL_UART_MspInit()`两个函数本质上是不同的。`HAL_UART_MspInit()`函数在`HAL_UART_Init()`函数中被调用，确保底层硬件资源的正常初始化。这种分离的设计可以使得应用程序层与底层硬件资源之间进行解耦，从而提高代码的可维护性和可移植性。

值得注意的是：

> 1、开启接收中断
>
> ```
> HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);
> ```
>
> 1.首先判断是否已经在进行接收操作，如果是，则返回 `HAL_BUSY`，否则继续执行。 2.检查 `pData` 和 `Size` 参数是否为空或为0，如果是，则返回 `HAL_ERROR`。 3.对UART进行加锁（因为是多任务环境下，可能会有多个任务同时在使用UART，需要进行加锁）。 4.将用户提供的接收缓冲区指针 `pData` 和缓冲区大小 `Size` 存到UART句柄结构体变量的成员变量中。
>
> 这个缓存区大小需要根据需求自行设置 5.清除错误码，并将接收状态设置为 `HAL_UART_STATE_BUSY_RX`。 6.解锁UART。 7.使能UART的奇偶校验错误中断、帧错误中断、噪声错误中断、接收数据寄存器非空中断。 8.返回 `HAL_OK`。 该函数的主要作用是设置UART的接收参数，并使能相应的中断，等待数据到来。当数据到来时，会触发UART接收中断，中断服务程序会把接收到的数据存储到用户提供的接收缓冲区中，在中断服务程序执行完毕后，用户就可以在应用程序中读取接收缓冲区中的数据了。
>
> 2、与之类似的，开启发送
>
> ```
> HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
> ```

**中断与回调**

HAL库的串口接收嵌套了较多层，关系比较复杂，

* `void USART1_IRQHandler(void)`是串口中断服务函数（这里宏定义重命名为`USART_UX_IRQHandler`），是当发生串口中断后首先调用的函数

```c
void USART_UX_IRQHandler(void)
{
#if SYS_SUPPORT_OS                                                   /* 使用OS */
    OSIntEnter();
#endif
    HAL_UART_IRQHandler(&g_uart1_handle);                               /* 调用HAL库中断处理公用函数 */

    while (HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE) != HAL_OK)     /* 重新开启中断并接收数据 */
    {
        /* 如果出错会卡死在这里 */
    }

#if SYS_SUPPORT_OS                                                   /* 使用OS */
    OSIntExit();
#endif
}
```

* 在中断调用`HAL_UART_IRQHandler(&UART1_Handler)`;这是一个HAL库函数，该函数是一个HAL库函数,在该函数中调用`UART_Receive_IT(huart);`,在函数 `UART_Receive_IT`中会把数据保存在串口句柄的成员变量`pRxBuffPtr`缓存中，同时`RxXferCount` 计数器减 1（`其实我没找到减1的语句`）。如果我们设置 `RxXferSize`=10,那么当接收到 10 个字符之后， `RxXferCount` 会由 10\
  减到 0（ `RxXferCount` 初始值等于 `RxXferSize`），这个时候再调用接收完成回调函数\
  HAL\_UART\_RxCpltCallback 进行处理。
* 也就是说HAL\_UART\_RxCpltCallback是在中断接收完成之后处触发的。

![](https://s2.loli.net/2023/03/14/WlSTC9UqOP4sroD.png)

总的来说用HAL库的回调去实现串口收发还是有点复杂，嵌套太多层，其实可以不使用这些回调函数，直接在中断向量服务函数实现自己的任务逻辑即可

![image-20230417143332047](https://s2.loli.net/2023/04/17/DcFbNxUf3n1w9X7.png)

#### 不使用回调的串口中断服务

以下代码编写了一个接收协议，接收到的数据必须是0x0d（回车/r的ASCII码13，对应的控制字符CR） 0x0a(换行/n的ASCII码)结尾

```
void USART1_IRQHandler(void)
{
	u8 Res;
	HAL_StatusTypeDef err;
#if SYSTEM_SUPPORT_OS	 	//使用OS
	OSIntEnter();
#endif
	if((__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_RXNE)!=RESET))  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
	{
		Res=USART1->DR;
		if((USART_RX_STA&0x8000)==0)//接收未完成
		{
			if(USART_RX_STA&0x4000)//接收到了0x0d
			{
				if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000;	//接收完成了
			}
			else //还没收到0X0D
			{
				if(Res==0x0d)USART_RX_STA|=0x4000;
				else
				{
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
				}
			}
		}
	}
	HAL_UART_IRQHandler(&UART1_Handler);
#if SYSTEM_SUPPORT_OS	 	//使用OS
	OSIntExit();
#endif
}
```

主要使用到了\_\_HAL\_UART\_GET\_FLAG(**HANDLE**, **FLAG**)函数

```
__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_RXNE)!=RESET
```

这段代码是一个宏定义，用于检查串口（UART）外设的指定标志位是否被设置。

具体代码如下：

```
#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR & (__FLAG__)) == (__FLAG__))
```

该宏有两个参数：

**HANDLE** 指定了串口句柄：

其实就是前面初始化USART1的时候实例化的**UART\_HandleTypeDef**型的结构体

**FLAG** 参数可以取以下值之一：

* UART\_FLAG\_CTS：CTS 变化标志（UART4 和 UART5 不支持）
* UART\_FLAG\_LBD：LIN 中断检测标志
* UART\_FLAG\_TXE：发送寄存器为空标志
* UART\_FLAG\_TC：传输完成标志
* UART\_FLAG\_RXNE：接收寄存器非空标志
* UART\_FLAG\_IDLE：空闲线检测标志
* UART\_FLAG\_ORE：溢出错误标志
* UART\_FLAG\_NE：噪声错误标志
* UART\_FLAG\_FE：帧错误标志

#### 串口数据的发送
