4.1 数据传输方式
终端与其他设备通过数据传输进行通信。数据传输可以通过两种方式进行——串行通信和并行通信。
4.1.1 串行通信
串行通信是指使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度。其只需要少数几条线就可以在系统间交换信息,特别使用于计算机与计算机、计算机与外设之间的远距离通信。
串行数据传输时,数据是一位一位地在通信线上传输的,先由具有几位总线的计算机内的发送设备,将几位并 行数据经并-串转换硬件转换成串行方式,再逐位经传输线到达接收站的设备中,并在接收端将数据从串行方式重 新转换成并行方式,以供接收方使用。串行数据传输的速度要比并行传输慢得多,但对于覆盖面极其广 阔的公用电话系统来说具有更大的现实意义。
串行数据通信的方向性结构有三种:单工、半双工、全双工

单工:在任何时刻都只能进行一个方向的通讯,即一个固定为发送设备,另一个固定为接收设备;
举个简单的例子:单工就类似于我们经常见到的单向车道,只允许向一个方向进行。
半双工:两个设备之间可以收发数据,但不能在同一时刻进行;
举个简单的例子:半双工类似于一个窄路双向车道,每次只允许向一个方向前进,两边不能同时行进,否则就会把 路堵死。
全双工:在同一时刻,两个设备之间可以同时收发数据。
举个简单的例子:全双工类就像我们的正常双向车道,车流可以同时往两个方向行进。
4.1.2 并行通信
并行通信是指在计算机和终端之间的数据传输通常是靠电缆或信道上的电流或电压变化实现的。如一组数据的各数据位在多条线上同时被传输。
并行通信时数据的各个位同时传送,可以字或字节为单位并行进行。并行通信速度快,但用的通信线多、成本高,故不宜进行远距离通信。计算机或plc各种内部总线就是以并行方式传送数据的。另外,在PLC底板上,各种模块之间通过底板总线交换数据也以并行方式进行。
并行通信传输中有多个数据位,同时在两个设备之间传输。发送设备将这些数据位通过 对应的数据线传送给接收设备,还可附加一位数据校验位。接收设备可同时接收到这些数据,不需要做任何变换就可直接使用。并行方式主要用于近距离通信。计算机内的总线结构就是并行通信的例子。这种方法的优点是传输速度快,处理简单。

4.1.3 并行通信和串行通信的比较

4.2 初识串口
串口,全名叫做串行端口(Serial port)。
串口根据通讯的数据同步方式,又分为同步和异步两种,接下来我会给大家详细介绍。
4.2.1 如何查看串口
大家现在可以跟随我的操作来查看自己电脑的串行端口号。
首先右击我们的此电脑,然后单击我们的管理选项,单机设备管理器,单击端口,这样就可以看到我们的端口号辣!



在这里,大家就可以看到之间的端口号辣!这里需要大家注意的是,每台电脑的端口号都不相同,在我电脑上的端口号是这俩,但是在你们电脑上可能是其他的端口号,那么应该如何确定自己所需的端口号呢?
其实非常简单,我们只需要将我们的CH340拔掉,等待界面刷新,就会发现少了一个端口号,而这个缺失的端口号就是我们的CH340的端口辣!是不是非常简单。最后我们可以记录一下CH340所对应的端口号,以免后面记错。
4.2.2 串口的种类与作用
4.2.2.1 串口的种类
串口分为很多很多种类别,例如我们家里日常使用的网线接口,也是名为RJ-45的串口;

又比如我们每天都需要用到的USB接口,USB全称是Universal Serial Bus(通用串行总线)也是串口。

不过今天,我们要聊的是在单片机中最常见的串口——TTL串口。
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)——通用 同步或异步 接收器/发送器。我们今天所用的TTL串口就是其中的Universal Asynchronous Receiver/Transmitter(通用异步接收器/发送器)。所以有时候我们也会见到UART这种缩写,即为通用异步接收器/发送器。
TTL串口仅需两根数据线即可完成两个设备的双向通讯。一条从发送数据的IO口连接到接收数据的IO口,另一条反之。两设备在数据线上按照约定好的频率切换高低电平,代表0和1,即可完成通信。
用来发送的IO口我们称为TX(Transmit),用来接收的IO口我们称为RX(Receive)。注意:在使用串口时不是一台设备的TX与另一台设备的TX连接,而是一台设备的TX与另一台设备的RX连接。

除此之外,我们在连接线路时还需要对两设备进行共地。需要共地是因为我们平常所说的电压其实是一个相对值。就是说我们规定一个电势点为0V,则其他电压其实是相对于此电势点的差值。倘若两设备对0V参考点的定义不一样,则对高低电平的理解也会不一样,,就会无法完成正常的通信。此时只要我们用导线将两设备的地连接在一起,那么他们的0V参考点就在同一水平上,就能实现正常的通信。

4.2.2.2 串口的作用
我们要知道我们为什么需要使用串口?
通信的目的:硬件之间通过数据进行互相的交流,串口通信将一个设备的数据传送到另一个设备,以达到扩展硬件系统的目的
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
串口通信协议是指规定了数据包的内容,包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。
4.2.3 串口参数及串口工作原理
4.2.3.1 串口参数
通讯速率(波特率与比特率)。
衡量通讯性能的一个非常重要的参数就是通讯速率,通常以比特率(Bitrate)来表示,即每秒钟传输的二进制位数,单位为比特每秒(bit/s)。在这里给大家补充一个小知识点:1byte = 8bit,即1字节=8比特。
容易与比特率混淆的概念是“波特率”(Baudrate),它表示每秒钟传输了多少个码元。而码元是通讯信号调制的概念,通讯中常用时间间隔相同的符号来表示一个二进制数字,这样的信号称为码元。如常见的通讯传输中,用0V表示数字0,5V 表示.数字1,那么一个码元可以表示两种状0和1,所以一个码元等于一个二进制比特位,此时波特率的大小与比特率一致;如果在通讯传输中,有0V、2V、4V 以及 6V 分别表示二进制数 00、01、10、11,那么每个码元可以表示四种状态,即两个二进制比特位,所以码元数是二进制比特位数的一半,这个时候的波特率为比特率的一半。
因为很多常见的通讯中一个码元都是表示两种状态,人们常常直接以波特率来表示比特率,虽然严格来说没什么错误,但希望大家能了解它们的区别。
4.2.3.2 串口工作原理
4.2.3.2.1 寄存器
数据的接收与发送工作主要是由发送数据寄存器、发送移位寄存器和CPU完成,接下来让我们看一段小动画来了解串口在轮询模式下的具体的工作流程~
看完了这段动画,相信大家对于轮询模式下串口的收发有了一定的理解,接下来由我再帮大家梳理一下轮询模式下串口的收发过程。
首先,ESP32的CPU会依次将数据移到寄存器中,而发送移位寄存器中的数据则会按照我们设置的比特率将数据转换成高低电平从TX引脚输出,发送数据寄存器中的数据会在发送移位寄存器发送完成之后被移到发送移位寄存器进行下一次发送,随后CPU会检查发送数据寄存器中的数据是否已经移动到发送移位寄存器,然后接着将数据移动到发送数据寄存器中等待下一轮数据发送;同理,接收移位寄存器每接收完一帧,就将数据移到接收数据寄存器,而CPU会一直查询收数据寄存器中是否有新数据可读,一旦检测到,就马上把数据从寄存器移到我们用于接受数据的变量中。
那么以上就是我们的串口在轮询模式下的工作原理辣!除了轮询模式,串口还可以在中断模式下工作,大家不用着急,串口的中断模式在后面会为大家详细讲解。
4.2.3.2.2 时序图
串口时序图是指在串口通信过程中,用来描述数据传输过程中各个信号线状态变化和时间关系的图形表示。
组成部分:
- 起始位:标志一个数据帧的开始,固定为低电平,表示一个数据传输的开始
- 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
- 校验位:用于数据验证,根据数据位计算得来
- 停止位:用于数据帧间隔,固定为高电平,表示一个数据传输的结束
对于正逻辑的TTL电平,起始位是低电平,停止位为高电平。


- 时序图

4.3 硬件准备&接线
4.3.1 硬件准备
我们本次实验需要准备的硬件设备有:
1.ESP32开发板一个
2.面包板
3.红绿灯一个
4.CH340(USB-TTL)模块一个
5.杜邦线、跳线若干
6.Type-C数据线一条
7.CH340驱动安装
4.3.2 接线原理图
原理图概览:

我们本次使用的是ESP32的串口2,RX2的引脚是19,TX2的引脚是20。现在就请大家跟着我来操作,一起来学习一下怎么在文件中查看所需的引脚吧。
接下来让我带着大家一步一步的来完成所有硬件设备的接线。
4.4 数据发送&数据发送与接收
ok!我们的接线工作到目前为止已经全部完成了,给各位小伙伴们再留一分钟的时间,根据刚才的步骤再次检查一下接线是否正确、是否松动等问题~
贴段代码:
#include <Arduino.h>
#include <HardwareSerial.h>
void setup()
{
Serial2.begin(9600);
}
void loop()
{
if (Serial2.available())
{
Serial2.write(Serial2.read());
}
}
接下来让我们逐句的分析每行代码:
#include <HardwareSerial.h>
HardwareSerial
库是一个内置串口库,我们只需要在用的时候#include
这个头文件即可
Serial2.begin(9600);
- 这句代码是初始化
Serial2
,并将波特率设置为9600
- 来让我们一起看一下
Serial2.begin()
函数的具体定义: unsigned long baud
:这个参数是我们用来设置波特率的uint32_t config
:UART_PARITY_DISABLE
: 无校验位UART_PARITY_ODD
: 奇校验UART_PARITY_EVEN
: 偶校验UART_STOP_BITS_1
: 1个停止位UART_STOP_BITS_2
: 2个停止位UART_DATA_5_BITS
: 5个数据位UART_DATA_6_BITS
: 6个数据位UART_DATA_7_BITS
: 7个数据位UART_DATA_8_BITS
: 8个数据位- 例:8个数据位,1个停止位,无校验位
uint32_t config = UART_DATA_8_BITS | UART_STOP_BITS_1 | UART_PARITY_DISABLE
int8_t rxPin
:设置RX引脚int8_t txPin
:设置TX引脚bool invert
:用于指定是否需要反转串口的信号电平,默认false
即可unsigned long timeout_ms
:设置串口通信的超时时间,在读取数据时,如果指定时间内没有接收到新的数据,则认为读取操作超时uint8_t rxfifo_full_thrhd
:接受缓冲区最大阈值
if (Serial2.available())
{
Serial2.write(Serial2.read());
}
- 在
if
判断语句中,Serial2.available()
是用来查询串口2的接收缓冲区中是否有字节 - 若查询到有字节,则往下执行
Serial2.read()
,read()
函数的作用是从串行通信的接收缓冲区中取出一个字节的数据。在调用此函数时,如果缓冲区中有数据,它会立即返回并移除缓冲区中的一个字节;如果没有数据,它会立即返回-1
;read()
函数是阻塞的,它会等待直到有数据可读或者超时 - 最后我们将读取到的数据用Serial2.write
()
函数写入单片机
在这里,我们就需要提到CH340的用处。
CH340驱动就是USB转串口的驱动的一种,因为我们现在的电脑上,已经不存在串口,所以我们一般使用USB转串口芯片,目的只有一个,把电脑的USB口映射为串口用。常用的USB转串口芯片有CH340、CP2102、PL2303、FT232等。芯片是CH340的均可以使用。安装CH340驱动之后,我们使用的开发板子(单片机)连接串口就可以正常发挥其功能了。电脑usb电平转为TTL电平。
现在,我们可以先将CH340插到自己的电脑上,然后打开自己电脑的端口界面。还记得怎么操作吗?这里我再带大家操作一次。

这样,我们就可以看到我们的端口号了(PS:如果有同学不知道哪个是CH340的端口,可以将其先拔掉,刷新之后看一下哪个端口号会消失,记住并将其再插回原位置(不同的插口端口号各不相同哦!同学们一定要注意))
将上面的代码编译、烧录到单片机里面之后,我们来打开事先安装好的串口助手。

将波特率
设置为9600
,串口选择
选择我们刚刚提到的自己电脑的端口号,单击打开串口
,在发送框内打出hello world!
并发送,这时,我们所发送的数据会通过我们所选择的端口,通过CH340的TX引脚发送,数据沿着数据线(杜邦线)传输到单片机的RX引脚上并存到缓冲区中,由串口2读取。我们可以看到,串口的RX接收到了我们所发的字符!这就实现了单片机串口与电脑的通信。

4.5 串口中断
前面我们使用了轮询模式去接收我们所发的字符,但是,在轮询模式下,我们的CPU会一直被字符接收事件所占用,导致在这段时间中其他程序无法正常运行。当然,在我们日常学习中这一小段时间可能影响并没有这么大,但是在实际应用过程中这一小段时间则可能会导致非常严重的后果。于是,下面我们就要用到串口中断去打断CPU的等待,在上节课中我们已经了解了什么是中断,这里就不再过多赘述,那么,什么是串口中断呢?让我们先一起来看一段动画演示。
我们现在已经了解了中断的工作原理,接下来让我们来进行代码的实现。
贴段代码:
#include <Arduino.h>
#include <HardwareSerial.h>
String recieve_data = "";
//编写回调函数
void callback(String command)
{
//需要去处理的事件
if (command != "")
{
//接收到字符串"on"后所做的操作
Serial2.print("receive data from Serial2:");
Serial2.println(command);
}
}
//中断处理函数
void serialEvent2()
{
while (Serial2.available())
{
char indata = (char)Serial2.read();
if (indata == '\r')
{
callback(recieve_data);
//清空接收缓冲区,为下一次接收做准备
recieve_data = "";
}
else
{
recieve_data += indata;
}
}
}
void setup()
{
Serial2.begin(9600);
}
void loop()
{
//处理完中断回调函数之后继续执行主循环
Serial2.println("looping...");
delay(1000);
}
下面让我们来一起分析这段代码:
- 首先让我们 来看一下
serialEvent2()
函数,我们首先选中Serial2
,右键,点击转到声明,这样我们就打开了HardwareSerial.h
文件,往下滑,在第166到第182行可以看到我们串口2的TX引脚与RX引脚所对应的GPIO口了。然后返回main.c
,选中Serial2
,右键,点击转到定义,这样我们就进入了HardwareSerial.cpp
文件,我们在这里可以找到关于串口的所有函数。 - 在第25-36行,我们可以看到这么几行代码
void serialEvent(void) __attribute__((weak));
void serialEvent(void) {}
#if SOC_UART_NUM > 1
void serialEvent1(void) __attribute__((weak));
void serialEvent1(void) {}
#endif /* SOC_UART_NUM > 1 */
#if SOC_UART_NUM > 2
void serialEvent2(void) __attribute__((weak));
void serialEvent2(void) {}
#endif /* SOC_UART_NUM > 2 */
serialEvent
、serialEvent1
与serialEvent2
分别对应的是板载串口、串口1与串口2的中断处理函数,上节课我们已经讲过,中断处理函数是当满足一定条件时自动调用的函数。当串口2的缓冲区有字节时,调用Serial2.available()
函数,产生串口中断
String recieve_data = "";
- 我们定义了一个全局字符串变量
recieve_data
用于接取我们输入的字符串
char indata = (char)Serial2.read();
- 在这里我们定义了一个字符变量
indata
去接串口2读到的字节,并强制转换为字符类型
if (indata == '\r')
{
callback(recieve_data);
//清空接收缓冲区,为下一次接收做准备
recieve_data = "";
}
else
{
recieve_data += indata;
}
- 串口每接收到一个字符,都需要通过
if
语句判断接收到的字符是否为换行符'\r'
,若否,则将字符indata
加到recieve_data
变量中;若是,则调用回调函数callback()
void callback(String command)
{
//需要去处理的事件
if (command != "")
{
//接收到字符串"on"后所做的操作
Serial2.print("receive data from Serial2:");
Serial2.println(command);
}
}
- 这部分是回调函数的内容,当满足
if
判断语句时,则串口2打印receive data from Serial2
- 最后,应清空接收缓冲区,为下一次数据接收做准备
4.6 使用串口点亮红绿灯
现在,我们已经了解了中断的原理以及如何使用串口中断,那么现在就让我们来动手直观的感受一下串口中断吧!
接下来,我们将在loop()
循环中使用串口持续打印字符串looping...
,然后我们将使用串口中断点亮一个红绿灯。当发送<LED:on>
时,红灯亮,并使用串口打印"LED is ON"
,loop()
循环将被打断,同理,当发送字符串<LED:off>
时灯将被熄灭,并使用串口打印"LED is OFF"
,如果发送的是其他命令,则打印"Unknown LED command!"
。下面让我们来一起动手实践一下吧!
贴段代码:
#include <Arduino.h>
#include <HardwareSerial.h>
#include <string.h>
#define PIN9 9
String receive_data = "";
int state = 0;
// 中断回调函数:解析命令并控制LED
void ledcontrolCallback(String command)
{
// 查找键和值之间的分隔符 ":"
int middle_index = command.indexOf(':');
if (middle_index == -1)
{
Serial2.println("Invalid command format!");
return;
}
// 提取键和值
String key = command.substring(0, middle_index); //提取command从索引值0到索引值index前的所有字符
String value = command.substring(middle_index + 1); //提取command从索引值index+1到末尾所有字符
// 检查键是否为 "LED"
if (key == "LED")
{
if (value == "on")
{
digitalWrite(PIN9, HIGH);
Serial2.println("LED is ON");
}
else if (value == "off")
{
digitalWrite(PIN9, LOW);
Serial2.println("LED is OFF");
}
else
{
Serial2.println("Unknown LED command!");
}
}
else
{
Serial2.println("Unknown key!");
}
}
//中断处理函数
void serialEvent2(void)
{
//当串口2的接收缓冲区有字节时
while (Serial2.available())
{
char inchar = (char)Serial2.read();
if (state == 0)
{
if (inchar == '+')
{
state = 1;
receive_data = "";
}
else
{
state = 0;
receive_data = "";
}
}
else if (state == 1)
{
if (inchar == '-')
{
state = 0;
Serial2.print("Received data:");
Serial2.println(receive_data);
ledcontrolCallback(receive_data);
receive_data = "";
}
else
{
receive_data += inchar;
}
}
else
{
Serial2.print("ERROR!");
}
}
}
void setup()
{
Serial2.begin(9600);
pinMode(PIN9, OUTPUT);
}
void loop()
{
// loop函数可以保持为空,因为数据将通过serialEvent()函数处理
Serial2.println("looping...");
delay(1000);
}
下面让我们来一起分析这段代码:
4.7 小结
在本节课程中,我们学习到了:
- 1.串口的定义
- 2.串口的作用
- 3.串口的数据收发
- 4.串口中断以及使用
最后,希望同学们能够在课后抽出一小部分时间温故而知新,认真独立完成课后作业,”三天不读口生,三天不练手生”,大家继续加油!