跳转至

UART串口通信

概要

在本节课程1Z实验室为大家讲解串口通信的接线方式,ESP32中的串口UART资源与相关API, 并给出了一个UART的小应用实例。

keywords uart communication micropython-esp32 usb2ttl

什么是串口通信

串口通信的英文缩写是UART(Universal Asynchronous Receiver Transmitter) 全称是通用异步收发器。

定义都是一些看似简单实则难以理解的文字。

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

异步收发

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

波特率

概念

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

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

波特率机制潜在的问题

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

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

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

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

针对于波特率的改进

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

做个不恰当的比方:

  • 靠约定相同的波特率来保持收发同步,就像是两个人约定着互相数自己的脉搏来计时,每跳动一次就接收一比特的数据,极其不靠谱。

  • 而带有时钟信号线的通信系统,就如同在两个人面前放了一块表,都按照秒针的转动来同步自己的收发,有了统一的参考,通信过程就变得靠谱多了。

我们后面的章节中即将提到的I2C总线协议,以及SPI总线协议,都是以时钟线来保证信息的收发同步的。但是我们本节的主角,最基本的串口通信,是不具备这一特性的。笔者在这里稍作科普,方便大家对后面的通信协议的理解。接下来言归正传,继续讲解串口通信。

串口通信协议的数据帧

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

起始位

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

数据位

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

奇偶校验位

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

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

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

  • 偶校验,校验位为逻辑0。整个数据帧中逻辑1的个数为偶数,则满足校验。

相信你已经猜到了奇偶校验位是干什么的了。没错,就是靠数一数每一帧的数据里逻辑1的个数是偶数还是奇数,来简单的判断数据在发送过程中是否有出错。是的,这种方式乍看起来十分靠谱,因为传输过程中只要有一位出错,都能够检测出来。除非。。。校验位出错了!

停止位

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

说了这么多概念,你可能一脸懵逼。我们以传送一个大写字母A为例,直接上波形图:

结合上面的概念,再来看这张图,是不是一切都很明了了呢。

传输方向

等等,还没有结束。在串口通信以及之后要讲的几种总线协议中,存在传输方向这一概念。

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

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

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

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

如果你还想继续巩固对串口通信的理解,可以在B站上观看串口通信科普视频.

最简单的UART串口接线

如下图所示:
串口连接图.png

RX 代表信息接收端,TX 代表信息发送端。

硬件资源

NodeMCU-32S开发板中有三组支持串口的GPIO:

第一组是 TX0 和 RX0,这一组串口资源被REPL所占用,所以无法被用户所使用。

组号 RX TX
0 GPIO3 GPIO1
1 GPIO9 GPIO10
2 GPIO16 GPIO17

不同于其他MicroPython开发板,ESP32还可以自定义GPIO作为UART,只要该GPIO满足以下关系:

作为TX的GPIO能够进行输出

作为RX的GPIO能够作为输入

显然,几乎所有符合条件的GPIO都可以作为串口的输入 RX,

除了34,35,36,39这四个GPIO只能作为输入外,其余所有的GPIO理论上都可以作为输出 TX

UART的API文档

UART构造器

导入UART 模块

from machine import UART

UART对象的构造器函数:

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

使用ID直接构造

上面我们说过,UART的id只能取0,1,2的,我们可以通过id来直接构造这三组串口:

>>> from machine import UART
>>> a = UART(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: UART(0) is disabled (dedicated to REPL)
>>> a = UART(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: UART(3) does not exist

以上的代码证明,我们确实无法使用第0组UART资源,他被REPL所占用,以及我们无法给串口id赋予大于2的id

构造第一组串口资源:

>>> b = UART(1)
>>> b
UART(1, baudrate=115201, bits=8, parity=None, stop=1, tx=10, rx=9, rts=-1, cts=-1, timeout=0, timeout_char=1)
>>>

构造第二组串口资源:

>>> c = UART(2)
>>> c
UART(2, baudrate=115201, bits=8, parity=None, stop=1, tx=17, rx=16, rts=-1, cts=-1, timeout=0, timeout_char=1)
>>>

我想细心的你已经发现,我们只需要简单的传入id为1或2即可初始化构造出我们的两组串口硬件资源,txrxGPIO编号都打印了出来,和上文中的硬件资源表格中笔者的标注是一一对应的。

默认的波特率为115201,近似约等于115200,这个数值取决于各个芯片的精度,介于UART的协议存在一定的容错空间,我们将默认的波特率视为115200即可

更改管脚映射的构造

可能有时候你的需要使用别的管脚,不希望使用默认的GPIO资源。

以13号GPIO和12号GPIO编号为例,我们修改rxtx的管脚映射,来构造一个UART对象:

from machine import UART
d = UART(2, baudrate=115200, rx=13, tx=12, timeout=10)

函数

在接下来的示例中, 我们构造id=1uart对象来列举UART对象的函数。

uart = UART(1)

uart.read(length)

函数说明:从串口读取指定长度的数据并返回,若长度未指定则读取所有数据。

length: 读入的字节数

示例:

>>> uart.read(10)         # 读入10个字符

uart.readline()

函数说明:从串口读取一行数据

示例:

>>> uart.readline()      # 读入一行

uart.readinto(buf)

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

buf: 缓冲区

示例:

uart.write(data)

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

data: 需要写入(发送)的数据

示例:

uart.write('abc')    # 向串口写入3个字符abc

uart.any()

函数说明: 检查是否有可读的数据,返回可读数据的长度

示例:

uart.any()          # returns the number of characters waiting

ESP32串口自发自收实验

接线 将开发板的 13号引脚与12号引脚用杜邦线相连接

from machine import UART
from machine import Timer
import select
import time

# 创建一个UART对象,将13管脚和12管脚相连
# 为什么不适用UART1 默认的管脚? 亲测在默认的 9,10号管脚下存在发送会触发重启的bug
uart = UART(1, rx=13, tx=12)

# 创建一个Timer,使用timer的中断来轮询串口是否有可读数据
timer = Timer(1)
timer.init(period=50, mode=Timer.PERIODIC, callback=lambda t: read_uart(uart))


def read_uart(uart):
    if uart.any():
        print('received: ' + uart.read().decode() + '\n')


if __name__ == '__main__':
    try:
        for i in range(10):
            uart.write(input('send: '))
            time.sleep_ms(50)
    except:
        timer.deinit()

示例输出:

深入学习

上文讲解了如何使用ESP32的UART资源,如何发送与接收字符串。 如果后续深入学习的话,可能还涉及到:

  • PC串口调试助手的使用

  • 自定义二进制通信协议,编写自己的上位机

  • 使用PySerial让PC与ESP32进行串口通信