# UART串口通信

  - [概要](#概要)
    - [什么是串口](#什么是串口)
      - [异步收发](#异步收发)
      - [物理连线](#物理连线)
      - [波特率](#波特率)
        - [概念](#概念)
        - [波特率机制潜在的问题](#波特率机制潜在的问题)
        - [针对于波特率的改进](#针对于波特率的改进)
      - [串口通信协议的数据帧](#串口通信协议的数据帧)
        - [起始位](#起始位)
        - [数据位](#数据位)
        - [奇偶校验位](#奇偶校验位)
        - [停止位](#停止位)
      - [传输方向](#传输方向)
    - [UART端口通用方法集合](#uart端口通用方法集合)
  - [UART引脚定义](#uart引脚定义)
  - [machine.UART API详解](#machineuart-api详解)
    - [宏定义](#宏定义)
    - [类](#类)
      - [构造UART](#构造uart)
    - [函数](#函数)
        - [初始化](#初始化)
        - [写数据](#写数据)
        - [读数据](#读数据)
        - [释放资源](#释放资源)
  - [示例](#示例)

## 概要

  本节讲解串口通信的接线方式。

### 什么是串口

![img](Image/serial-kp.gif)

- **串口通信的英文缩写是UART(Universal Asynchronous Receiver Transmitter) 全称是通用异步收发器。**是通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输。
- UART应用比较广泛,常用于输出打印信息,也可以外接各种模块,如GPS、蓝牙等。

  但是,听起来很高深的概念,其实就是上面gif里的模型,两个设备,一根线串起来,发送方在线的一头将数据转换为二进制序列,用高低电平按照顺序依次发送01信号,接收方在线的另一头读取这根信号线上的高低电平信号,对应转化为二进制的01序列。

#### 异步收发

  为什么叫异步收发呢?因为如果给上图的两个设备再加一根线,比如下图,让左边的设备也可以成为接收方,右边的设备也可以成为发送方,那么对于左右两个设备而言,发送和接受便可以在两根线上同时进行,所以说,发送和接收是异步的。

![img](Image/serial-two-wire.png)

#### 物理连线

- 两个UART设备的连接示意图如下,UART与其他模块一般用2线相连,它们分别是:

  - TX:发送数据端,和对端的RX相连;

  - RX:接收数据端,和对端的TX相连;


<div align=center>
图1 2线UART设备连接示意图
</div>

![img](Image/zh-cn_image_0000001053926237.png)

- UART通信之前,收发双方需要约定好一些参数:波特率、数据格式(起始位、数据位、校验位、停止位)等。通信过程中,UART通过TX发送给对端数据,通过RX接收对端发送的数据。

#### 波特率

##### 概念

&emsp;&emsp;**波特率(`bandrate`)**是指,每秒钟我们的串口通信所传输的bit个数,通俗的讲就是在一秒内能够发送多少个1和0的二进制数。

&emsp;&emsp;比如,波特率是9600,就意味着1S中可以发送9600个0和1组成的二级制序列。

##### 波特率机制潜在的问题

&emsp;&emsp;在串口通信的过程中,波特率直接影响着通信的速率,**而且在发送端和接收端必须保证同样的波特率,这样才能在一定程度上保证发送和接收保持同步**。

&emsp;&emsp;然而实际上在发送方和接收方波特率不匹配或者波特率较高的时候,或者有信号干扰,很可能发送和接收的顺序会发生错乱,导致数据的溢出,就像下面的动图中展示的那样——发送方波特率高于接收方:

![img](Image/serial-miss.gif)

> 暂时请忽略上面的DTR和DSR所连接的线,只借此图说明串口通信中波特率不一致或波特率极高的情况下会出现的数据丢失问题。

&emsp;&emsp;当然一般在9600和115200的波特率下,上面的情况几乎不会发生,但是这种靠发送接收速率来保持数据同步的方式显然还是存在缺陷的。

##### 针对于波特率的改进

&emsp;&emsp;为了改进上面的问题,很多基于串口通信的协议都会加上一条时钟信号线,信息的发送和接受,发送方和接收方都要完全按照时钟信号线上的信号来执行,这样,有了第三者 的介入,就使得数据发送和接受能够完全的保证同步。

&emsp;&emsp;做个不恰当的比方:

- 靠约定相同的波特率来保持收发同步,就像是两个人约定着互相数自己的脉搏来计时,每跳动一次就接收一比特的数据,极其不靠谱。
- 而带有时钟信号线的通信系统,就如同在两个人面前放了一块表,都按照秒针的转动来同步自己的收发,有了统一的参考,通信过程就变得靠谱多了。

#### 串口通信协议的数据帧

![img](Image/uart-basic-pdu.png)

&emsp;&emsp;在串口通信中,最基本的一帧数据,至少包含了 `起始位+数据位+停止位`,就如同上图红线框中所示。

##### 起始位

&emsp;&emsp;起始位(Start)是必须存在的,他必须是一个bit位的低电平,即逻辑0,标志着数据传输即将开始。

##### 数据位

&emsp;&emsp;紧随其后的便是数据位(Data Bits)。数据位包含了通信中的真正有效的信息,自然也是必须存在的。通常我们在配置串口选项时可选的值为 `6`, `7`, `8`, `9`,默认`8` ,这些数字标识了数据位的位数: 数据位的位数由通信双方共同约定,一般可以是6位、7位或8位,比如标准的ASCII码是0~127(7位),扩展的ASCII码是0~255(8位),因此通信的内容如果都是ASCII码的话,8位的2进制数构成的编码范围0-255即可表示完整的ASCII码字符集。

##### 奇偶校验位

&emsp;&emsp;校验位(Parity)可有可无。如果在通信协议的配置中,规定没有校验位,则数据位后直接跟随停止位。

&emsp;&emsp;如果设置了校验位,则有奇偶两种校验方式:

- 奇校验,校验位为逻辑1。整个数据帧中逻辑1的个数为奇数,包括奇校验位本身的逻辑1,则满足校验。
- 偶校验,校验位为逻辑0。整个数据帧中逻辑1的个数为偶数,则满足校验。

&emsp;&emsp;奇偶校验位靠数一数每一帧的数据里逻辑1的个数是偶数还是奇数,来简单的判断数据在发送过程中是否有出错。这种方式在传输过程中只要有一位出错,都能够检测出来。除非校验位出错了!

##### 停止位

&emsp;&emsp;停止位(Stop Bit)是一帧数据的结束标志,停止位将信号线置位高电平。可以是1bit、1.5bit、2bit的高电平。可能大家会觉得很奇怪,怎么会有1.5位~没错,确实有的。一般我们常选1bit,这样尽可能的压缩了一帧数据的体积,提高了传输的速率。

&emsp;&emsp;以传送一个大写字母A为例,波形图如下:

![img](Image/uart-pdu-wave.png)

#### 传输方向

&emsp;&emsp;在串口通信以及之后要讲的几种总线协议中,存在**传输方向**这一概念。

- MSB(Most Significant Bit: 最高有效位), 指从一个字节的最高位向最低位的顺序依次传输
- LSB(Least Significant Bit:最低有效位),指从一个字节的最低位向最高位的顺序依次传输

&emsp;&emsp;通常,默认都是MSB的传输方向。上面的字母A的波形图,便是MSB。

&emsp;&emsp;如果是LSB,则A的Ascii码的区间里,从左至右的`0100 0001`的顺序就需要颠倒过来,写成`1000 0010`

### UART端口通用方法集合

&emsp;&emsp;UART接口定义了操作UART端口的通用方法集合,包括:

- 获取、释放设备句柄
- 读写数据、获取和设置波特率
- 获取和设置设备属性。

## UART引脚定义

&emsp;&emsp;有14个引脚可以用作UART

&emsp;&emsp;waffle nano开发板中有三组支持串口的GPIO,第一组是 TX0 和 RX0,这一组串口资源被REPL所占用,所以无法被用户所使用。

&emsp;&emsp;waffle nano还可以自定义GPIO作为UART,只要该GPIO满足以下关系:

> 作为TX的GPIO能够进行输出
>
> 作为RX的GPIO能够作为输入

| UART组号 | TX            | RX            |
| -------- | ------------- | ------------- |
| UART 0   | pin 3、pin 13 | pin 4、pin 14 |
| UART 1   | pin 0、pin 6  | pin 1、pin 5  |
| UART 2   | pin 11        | pin 12、pin 9 |

##  machine.UART API详解

&emsp;&emsp;使用`from machine import UART`导入`machine`模块的串口通信类`UART`

&emsp;&emsp;再使用`TAB` 按键来查看`Pin`中所包含的内容:

```python
>>>from machine import UART
>>>UART.
read            readinto        readline        write
PARITY_EVEN     PARITY_NONE     PARITY_ODD      bufempty
deinit          init            isbusy
```

### 宏定义

&emsp;&emsp;下面的宏定义用于配置uart,设置校验位。

| 宏定义           | 含义         |
| :--------------- | :----------- |
| UART.PARITY_EVEN | 校验位偶校验 |
| UART.PARITY_NONE | 不设置校验位 |
| UART.PARITY_ODD  | 校验位奇校验 |

### 类

&emsp;&emsp;`class UART(id[, baudrate, databits, parity, rx, tx, stopbit, timeout])`

- `id` — 串口编号,可用的UART资源只有两个, id有效取值为 `1`,2
- `baudrate` — 波特率,常用波特率为:`9600` `115200`, 默认为`115200`
- `databits` — 数据位,是通信中的真正有效信息的每一个字节单元所包含的比特位数。可选的值为 `6`, `7`, `8`, `9`,默认`8` 。 数据位的位数由通信双方共同约定,一般可以是6位、7位或8位,比如标准的ASCII码是0~127(7位),扩展的ASCII码是0~255(8位)
- `parity` — 基础校验方式 ,`None`不进行校验,`1` 奇校验 `0`偶校验
- `rx` — 接收口的GPIO编号
- ` tx`—发送口的GPIO编号
- `stopbit`—停止位个数, 有效取值为`1` ,`2`, 默认值为`1`
- `timeout`—超时时间,取值范围: `0 < timeout ≤ 2147483647`

#### 构造UART

&emsp;&emsp;用UART对象的构造器函数

&emsp;&emsp;示例:

```python
>>> from machine import UART,Pin
>>> uart = UART(1, baudrate=9600,parity=0,rx=Pin(5),tx=Pin(6),timeout=10)  #配置UART 1波特率为9600,偶校验,接收口的引脚为5,发送口的引脚为6,超时时间为10
>>>uart
UART(1, baudrate=9600, databits=8, parity=0, stopbits=1, tx=6, rx=5)
```

&emsp;&emsp;使用ID直接构造

&emsp;&emsp;UART的id只能取`0,1,2`的,可以通过id来直接构造这三组串口。无法使用第0组UART资源,他被python的REPL所占用,以及我们无法给串口id赋予大于2的`id`

&emsp;&emsp;示例:

```python
>>> from machine import UART
>>> uart0 = UART(0)#无法构造UART0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: UART(0) is disabled (dedicated to REPL)
>>> uart3 = UART(3)#无法构造UART3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: UART(3) does not exist
>>>uart1=UART(1)#构造UART1
>>>uart1
UART(1, baudrate=115200, databits=8, parity=None, stopbits=1, tx=6, rx=5)
>>>uart2=UART(2)#构造UART1
>>>uart2
UART(2, baudrate=115200, databits=8, parity=None, stopbits=1, tx=11, rx=12)
```

### 函数

&emsp;&emsp;在接下来的示例中, 构造`id=1`的`uart`对象来列举UART对象的函数。

```python
>>>from machine import UART
>>>uart = UART(1)
```

##### 初始化

&emsp;&emsp;`uart.init(id, baudrate, databits, parity, stopbits, tx, rx)`

&emsp;&emsp;函数说明:初始UART串口

&emsp;&emsp;参数含义同上文UART对象的构造器函数一致

&emsp;&emsp;示例:

```python
>>>from machine import UART,Pin
>>>uart = UART(1)#构造UART1串口
>>>uart.init(baudrate=9600,parity=1,tx=Pin(6),rx=Pin(5),timeout=10)#初始化UART串口波特率为9600,奇校验,接收口的引脚为5,发送口的引脚为6,超时时间为10
>>>uart
UART(1, baudrate=9600, databits=8, parity=ODD, stopbits=1, tx=6, rx=5)
```

##### 写数据

&emsp;&emsp;`uart.write(data)`

&emsp;&emsp;函数说明:向串口写入(发送)数据,返回data的长度.

&emsp;&emsp;`data`: 需要写入(发送)的数据

​		示例:

```python
>>>from machine import UART
>>>uart = UART(1)#构造UART1串口
>>>uart.write('abc')#向串口写入abc,返回数据长度3
3
```

##### 读数据

&emsp;&emsp;`uart.read(length)`

&emsp;&emsp;函数说明:从串口读取指定长度的缓冲数据并返回,若长度未指定则读取所有数据.

&emsp;&emsp;`length`: 读入的字节数

```python
>>>from machine import UART
>>>uart = UART(1)#构造UART1串口
>>>uart.read(2)#从串口缓冲区内读取两个字节的数据
>>>uart.read()#从串口缓冲区内读取所有数据
```

&emsp;&emsp;`uart.readline()`

&emsp;&emsp;函数说明:从串口读取一行缓冲数据

&emsp;&emsp;示例:

```python
>>>from machine import UART
>>>uart = UART(1)#构造UART1串口
>>> uart.readline()#读取一行数据
```

&emsp;&emsp;`uart.readinto(buf)`

&emsp;&emsp;函数说明:读入并且保存到缓冲区

&emsp;&emsp;`buf`: 缓冲区

&emsp;&emsp;示例:

```python
>>>from machine import UART
>>>uart = UART(1)#构造UART1串口
>>>buf=bytearray(8)
>>>uart.readinto(buf)
```

##### 判断串口缓冲区是否为空

&emsp;&emsp;`uart.bufempty()`

&emsp;&emsp;函数说明:不对串口缓冲区数据进行破坏性操作,判断串口缓冲区内数据是否为空。若串口缓冲区是否为空则返回`True`,若串口缓冲区存在未被读取的新数据,则返回`False`。

&emsp;&emsp;示例:

```python
>>>from machine import UART, Pin
>>>uart = UART(1)#构造串口UART
>>>uart.bufempty()#返回串口缓冲区是否为空
```


##### 判断串口是否为忙碌状态

&emsp;&emsp;`uart.isbusy()`

&emsp;&emsp;函数说明:判断串口是否处于忙碌状态。若处于忙碌状态则返回`False`,若处于空闲状态则返回`True`。

&emsp;&emsp;示例:

```python
>>>from machine import UART, Pin
>>>uart = UART(1)#构造串口UART
>>>uart.isbusy()#返回是否忙碌状态
```

##### 释放资源

&emsp;&emsp;`uart.deinit()`

&emsp;&emsp;函数说明:关闭UART,释放占用资源。

&emsp;&emsp;示例:

```python
>>>from machine import UART, Pin
>>>uart = UART(1)#构造串口UART
>>>uart.deinit()#关闭UART
```

## 示例

&emsp;&emsp;对waffle nano构建UART串口1,向串口写入(发送)数据,读取数据。

```python
from machine import UART,Pin
uart = UART(1, rx=Pin(5), tx=Pin(6))
uart.write(b'Hello World\r\n')
while True:
    print(uart.read())
```

&emsp;&emsp;第一行导入`machine`模块的硬件类`Pin`和串口类`UART`

&emsp;&emsp;第二行构造`UART`串口1,GPIO 5号引脚接收数据,GPIO 6号引脚传输数据

&emsp;&emsp;第三行向`UART`串口1传入数据

&emsp;&emsp;无限循环打印出uart串口1每行读取的数据