文章目录
概述
对于任何通信方式而言,通信协议的学习都是核心内容,没有之一。
因为无论使用的是哪一种通信接口,本质上都是在按照既定的通信协议,对数据进行收发和解析。
串口通信的通信协议相对比较简单,同时又几乎是最常用的通信方式,所以我们先来学习一下串口通信,作为通信学习的入门。
我们将从以下三个角度来分析串口通信协议:
- 串口通信的硬件接线方式。
- 串口通信的数据帧格式。
- 波特率问题。
最后本章节的末尾,我们还需要学习一下单片机USART外设的基本工作原理,再分析一下单片机引脚的使用及PC端接线。
通过以上内容的学习,我们就可以在下一章节中,实现单片机与 PC 端之间的双向串口通信。
串口通信的硬件接线方式
串口通信的硬件接线方式非常简单,具有以下特点:
- 常用异步串口通信中,通信只需要两条信号线就可以了。
- 点对点通信:UART 通常用于两个设备之间的通信,一个设备作为发送方(Tx),另一个设备作为接收方(Rx)。
- Tx和Rx交叉相连 :需将一方的 Tx 引脚连接到另一方的 Rx 引脚,再将另一方的 Tx 引脚连接回本方的 Rx 引脚。
尤其要注意接线方式是交叉相连,而不是对应相连,这是新手在接线时非常容易犯的错误!
假如我们要实现PC与单片机之间的串口通信,就需要把PC和单片机的Tx和Rx引脚交叉连接起来。如下图所示:

从这种硬件接线方式就可以看出,串口通信是一种全双工通信方式,通信双方可以在同一时刻同时发送和接收数据,而互不影响。
完成上图中的接线后,单片机如果想要向PC端发送数据,则可以控制Tx引脚:
- 输出高电平,表示发送一个bit数据1
- 输出低电平,表示发送一个bit数据0
如果单片机想要接收PC端发送的数据,则可以控制Rx引脚:
- 读取输入的高电平,就表示接收一个bit数据1
- 读取输入的低电平,就表示接收一个bit数据0
需要注意的是,在串口通信的接线中,通信双方还必须将 GND 引脚连接在一起 。也就是通信双方需要同时接地。
这是由于串口通信中使用高低电平来表示传输1/0,设备双方同时接地,可以保证具有统一的参考低电平。
而且同时接地,还可以减少外部干扰以及保障电路安全。
串口通信数据帧格式(重点)
UART通信采用的通信方式是串行通信 ,即数据按位(bit)顺序一位一位的传输。
而不管是发送端还是接收端,一次性处理数据的基本单位都是数据帧,所以我们首先就需要了解串口通信的数据帧格式。
一个经典的UART数据帧格式如下图所示:

下面基于这张图,我们来逐一讲解一下串口通信数据帧的组成。
通信空闲状态
即便物理层面的接线已经完成,也并不意味着通信会立刻开始。
在真正发送数据之前,通信总是会处于一种尚未开始传输数据的状态 ,这称为通信的空闲状态。
对于任何一种通信方式而言,其通信协议中都会明确规定:
在什么样的条件下,通信应当被认为是处于空闲状态。
那么,在串口通信中,空闲状态是如何表示的呢?
在串口通信协议中,明确规定:
当通信导线上保持为高电平时,表示串口通信处于空闲状态。
以 单片机与 PC 之间的串口通信 为例:
- 单片机的Tx引脚持续输出高电平,即表示通信还未开始,单片机当前没有向 PC 发送数据。
- 当单片机的 Rx 引脚持续检测到输入高电平 时,表示通信尚未开始,PC 当前没有向单片机发送数据。
也就是说,在串口通信中,通信双方在没有数据传输时,通信线路始终保持在高电平状态,以此作为通信空闲状态的标志。
起始位
既然在串口通信中,通信线路上持续保持高电平表示通信处于空闲状态,意味着通信尚未开始。
那么做什么能够表示通信开始了呢?
发送端又是通过什么方式,来告诉接收端"通信要开始了,可以准备接收数据了"呢?
答案是:
在串口通信中,发送端会通过 Tx 引脚发送1位 低电平,用这1位低电平来明确表示:一帧数据的传输即将开始。
这1位低电平就是通信开始的标志,这就是起始位。
接收端检测到这个起始位,就知道通信已经开始了。
当然起始位不属于要接收的数据,发送端在起始位后面紧跟着发送的一连串数据,称之为"数据位",才是真正发送的数据。
数据位
很显然,在串口通信中,数据位不可能只有 1 位,实际传输的数据长度也远不止一个 bit。
根据串口通信协议的规定,数据位的长度可以配置为 8 位或 9 位。
同时,为了提高数据传输的可靠性,还可以将数据位的最后1位设置为校验位(称之为带校验位),用于简单的数据错误检测。
于是串口通信的数据位配置,就有4种可能的选择:
- 8位,不带校验位。
- 8位,带校验位。
- 9位,不带校验位。
- 9位,带校验位。
如下图所示:

需要注意的是:
- 选择2和选择3会导致1个数据帧发送的数据不再是完整的字节,在程序中处理起来不符合程序员的直觉,比较少用。
- 选择4,可以发送1个字节数据,而且带1位校验位。但串口通信本身已经具有较高的稳定性可靠性,为了简化通信,通常不会带数据位。
所以,综合实际情况,选择1,8位数据位长度且不带校验位,是串口通信最常见的数据位选择。
停止位
既然有起始位,那就有停止位,它代表着一帧数据的结束,表示接收端已经接收到1帧完整数据。
根据串口通信的协议规定:
- 在数据位之后,发送端紧跟着发送1位 高电平,用这1位高电平来明确表示:一帧数据的传输已经完成了。
- 同时,这1位 高电平还会使通信线路重新回到高电平的空闲状态,这与空闲状态的设计也是自洽的。
- 当停止位发送完成后,一帧串口数据就完整发送结束了。
- 停止位的长度可选择0.5位、1位、1.5位以及2位,其中1位停止位是最常见的选择。
由此可以看出:
- 串口通信的 数据帧长度是固定且较短的
- 配置数据位是8位不带校验位的情况下,每一帧数据只能携带 1 个字节(8 bit)有效数据
- 如果需要发送多字节的数据内容,就必须 连续发送多个数据帧 来完成。
串口通信,无法在一个数据帧中同时发送多个字节的数据。
关于停止位,还有一个很有趣的问题,值得思考一番:
起始位是持续高电平后的1位低电平,所以很容易被接收端识别,那么终止位是如何识别的呢?
关键原因在于:串口通信中的数据帧格式是事先严格约定好的,而且是固定的。
例如,在采用 8 位数据、不带校验位 的配置时:
- 接收端在接收到起始位、约定好的数据位后
- 紧接着的1位就必须是高电平,也就是停止位
如果数据位后紧接着的不是高电平,也就不是停止位,那就说明这1帧数据在发送过程中出现了问题,必然是出错了。
所以,停止位的识别并不依赖电平的转换,而是依赖于数据帧格式的严格规定,依赖其约束力。
比特序(重要)
到此为止,我们已经完整地讲解了 串口通信的数据帧格式。
但串口通信中,还有一个非常重要的问题需要进一步解决:
串口通信是一种串行通信,数据并不是一次性整体发送的,而是 1 个 bit、1 个 bit 依次发送的。
那么,当我们通过串口发送数据时,待发送的多个 bit 数据,究竟应该按照什么顺序发送呢?
举一个例子:
对于整数88,以1个字节存储它时,存储它的补码形式:0101 1000
现在通过串口将整数88发送出去,那么在发送的1个数据帧中,其8位数据位,应该按照什么样的顺序发送呢?
从左往右?还是从右往左?
为了规范的回答这个问题,我们先提出一个概念------比特序。
比特序,用于在串行通信中,描述一个数据单元内部,bit位逐位发送的顺序。
比特序有两种最常见的形式:
- LSB First,即Least Significant Bit First,最低有效位优先。此时数据会从一个数据单元内部的最低有效位,发送到最高有效位结束。
- MSB First,即Most Significant Bit First,最高有效位优先。此时数据会从一个数据单元内部的最高有效位,发送到最低有效位结束。
串口通信的比特序
在串口通信协议中,明确规定:
- 串口通信的比特序采用LSB First,即数据位从最低有效位发送到最高有效位。
- 比特序在串口通信中,仅作用于数据位,数据帧其余部分皆已固定,和比特序无关。
仍然回到发送整数88这个例子:
发送端在发送整数88时,数据位的发送顺序是:
0 -> 0 -> 0 -> 1 -> 1 -> 0 -> 1 -> 0
每次讲到这里时,总有同学会问:
为什么串口通信会选择LSB First的比特序呢?
排除"别人设计时就是这么搞得,要你管,不服你自己设计通信协议",这种抬杠式的回答。
串口通信的比特序设计是有原因的:
- 兼容老旧设备,早期串行通信LSB First是主流设计。
- LSB First在硬件层面上比较易于实现。详情可以参考下面的小节《USART外设数据帧处理的工作原理》
串口多字节数据发送
要理解串口多字节数据发送,我们需要理解**"数据单元"**这个概念:
- 假如要发送的数据是88,那么88就是一个数据单元,数据单元的长度是1个字节。
- 假如要发送的数据是字符串"abc",那么该字符串整体就是一个数据单元,数据单元的长度是3个字节。
那么字符串"abc"这种多字节数据,应该怎么发送出去呢?
在通信传输数据时,发送"abc"肯定是:
- 先发a这1个字节
- 再发b这1个字节
- 最后发c这1个字节
这样才能保证接收端也是先接收a再接到b,最后收到c,如此就能收到一个字符串"abc"。
字符串"abc"转换成二进制表示,即转换成各个字符的编码值表示,各个字符的编码值是:
- 'a':十进制97,0x61,即二进制0110 0001
- 'b':十进制98,0x62,即二进制0110 0010
- 'c':十进制99,0x63,即二进制0110 0011
最后结合串口通信数据帧一次只能发送1个字节数据,以及串口比特序的概念,我们可以得出以下数据发送顺序:
- 第一个数据帧的数据位:1000 0110(发送字符a)
- 第二个数据帧的数据位:0100 0110(发送字符b)
- 第三个数据帧的数据位:1100 0110(发送字符c)
由此可见,串口在发送多字节数据时:
- 数据帧彼此是独立的,先发的数据帧对方先收到,数据帧的发送顺序由实际数据的字节顺序决定。
- 每一个数据帧内部的数据位,都依照"LSB First"的比特序逐位发送。
下面,我们可以把上面讲的整数88,以及字符串"abc"发送的时序图画出来,参考下面的小节。

数字88发送的时序图
数字88转换成二进制补码形式,数据是:0101 1000
在 8 位数据位、无校验、串口通信发送 1 个字节数据 88 时的时序图,如下所示:

字符串"abc"发送的时序图
字符串"abc"转换成二进制表示,即转换成各个字符的编码值表示,各个字符的编码值是:
- 'a':十进制97,0x61,即二进制0110 0001
- 'b':十进制98,0x62,即二进制0110 0010
- 'c':十进制99,0x63,即二进制0110 0011
字符串"abc"整体是一个待发送的数据单元,有3个字节,但每个数据帧只能发送1个字节。
所以abc三个字符作为三个数据帧依次发送,但各个数据帧内部的数据位按照"LSB First"从最低位发送到最高位。
在 8 位数据位、无校验、串口通信发送 3 个字节字符串数据 "abc" 时的时序图,如下所示:

校验位(奇偶校验)
校验位通常不带,包括我们讲串口通信时,也不会带校验位,但我们还是可以了解一下校验位。
了解一下校验位是如何工作的。
所谓数据校验,就是把数据位的最后一位作为校验位,然后发送端和接收端采用统一的校验方式来判断数据位是否发送正确。
注意校验位只能用于校验判断数据传输是否出错,它本身是不具备纠错功能的。
最简单、也比较常用的校验手段是奇偶校验。
进一步可以分为:
- 奇校验:要求数据位中有奇数个1
- 偶校验:要求数据位中有偶数个1

假如采用9位带校验的数据位格式,发送端发送数据85,采用奇校验,那么分析如下:
- 数字 85 的二进制表示中,数据位里一共有 4 个 "1" ,这是一个偶数。
- 采用奇校验的话,数据位的1应该是奇数个。
- 因此,校验位中的数据应该是1,这样才能使得数据位的1的个数变为奇数个。
如此,发送整数85的时序图,如下图所示:

然后,接收端接收到这一帧数据后,只需要校验数据位中1是否为奇数个,就可以粗略的判断数据传输是否有问题。
比如接收端接收到的数据帧如下图:

接收端发现接收到的这一帧数据中的数据位,1的个数变成了偶数个,那就说明数据传输出现了问题,可以要求发送端重传或者采取其它做法。
奇偶校验一定能发现数据传输中的错误吗?
当然不是,假如数据位在传输时,恰好有偶数个"1"变成了"0",那么数据位1总数的奇偶性就不会改变了,这样奇偶校验就失效了。
所以:
奇偶校验是一种实现简单、但检测能力有限的数据校验方式。
由于串口通信本身抗干扰能力较强,所以在实际使用串口通信时,可以选择不使用校验位,以简化通信过程。
数据帧格式设置
串口通信,数据帧格式如下图所示:

我们可以设置的地方有:
- 选择数据位的长度,8位或者9位。
- 数据位的最后一位是否有校验位,若有校验是奇校验还是偶校验。
- 停止位用几位,可以选择0.5位、1位、1.5位以及2位。
在后续的实验中,我们使用的数据帧格式是:数据位8位,不带校验位,停止位为1位。
这也是串口通信中,最常见、最通用、最"懒人友好"的数据帧格式。
工程中,我们习惯把它称之为"8N1"数据帧格式,其中8代表8位数据位,N代表无校验位(No Parity),1停止位。
波特率的问题(重点)
串口通信是一种异步串行通信协议,不需要通信双方共享统一时钟信号,通信时序是由双方自行控制的。
这样就出现了一个显而易见的问题:
数据帧的发送是依靠一定频率的高低电平变化来表示发送数据0和1,那么当发送端发送一个数据帧时,接收端如何才能准确识别每一个比特(bit)的时间间隔呢?
有点迷糊,没听懂这个问题,用人话来描述一下这个问题:
我给你写了一封信,信的内容如下:

那么我问你:
我这封的内容是"好",还是"女子"?
很明显大家都知道,这封信的内容是"好"。为什么呢?
因为依靠大家经验性的约定,上图内容中的文字间距和文字大小,显然不像是两个字,明显就只有一个字,所以信的内容就是"好"。
对于文字的阅读我们依靠经验约定,通信中数据的解析肯定不可能依靠经验约定,必然需要通信协议强制约定。
现在把这个例子等价到串口通信中,假如数据位采用8位无校验位,那么一帧数据的长度是10位:
假如发送端发送1个数据帧,用时1秒钟,那么就是每0.1秒发送1位,0.1秒这个间隔就是每位的间隔。如下图所示:

问题来了:
若接收端不知道0.1秒这个间隔,接收端能够正确解析数据帧吗?
显然是不可以的。
为了解决这个问题,这种串口通信引入了**波特率(Baud Rate)**的概念。
波特率可以理解成:通信中每秒传输的比特数量,单位是 bps(bits per second),即"每秒传输的比特数"。
既然知道1秒钟能传输多少个bit位,那么1个bit位究竟占多长时间就也可以确定了。
简单来说:
- 波特率 = 1秒内传输的bit数量
- 比如 波特率是9600 bps ,意味着每秒传输 9600 个 bit。
对于串口通信来说:
- 波特率用于衡量通信过程中的数据传输速率,在其它条件相同的情况下,波特率越大,传输效率越高。
- 波特率必须由双方统一约定好,双方都必须遵守,否则就会通信失败。
- 双方约定统一波特率,就相当于双方都知道1个bit位,究竟占据多长时间,这样接收端才能够正常解析数据。
在UART通信中,最常见的波特率是9600、115200、57600 等。
有了波特率这个概念后:
- 通信的发送端,知道每间隔多长时间发送1位数据。
- 通信的接收端,也知道每间隔多长时间接收解析1位数据。
现在你搞清楚波特率的概念,以及为什么需要波特率了吗?