第十章 UART实验
本章,我们将学习ESP32-S3的串口,教大家如何使用ESP32-S3的串口来发送和接收数据。本章将实现如下功能:ESP32-S3通过串口和上位机的对话,ESP32-S3在收到上位机发过来的字符串后,原原本本的返回给上位机。 本章分为如下几个小节: 10.1 串口简介 10.2 硬件设计 10.3 软件设计 10.4 下载验证
10.1 串口介绍 由于大部分Arduino开发板都没有调试功能,所以在开发中,最常用的就是串口打印信息到串口助手去调试程序。而串口打印,正规一点来讲就是串口通信,开发板通过串口外设把数据通过串口线传输到电脑的串口助手上位机。 在开发板上存在各种各样的通信,所以在这里花一点篇幅来讲解一下数据通信方面知识,方便大家对通信有点概念,知其然知其所以然。 10.1.1 数据通信的基本概念 这里将会简单讲解一下“串行/并行通信”、“单工/半双工/全双工通信”、“同步/异步通信”、“波特率”等知识。 串行/并行通信 平常经常会听到串行通信和并行通信,这两者其实很好去区分。而串口属于串行通信,特点就是数据是逐位按顺序依次传输,如下图所示。 图10.1.1.1 串行通信 并行通信,特点是数据各位通过多条线同时传输,如下图所示。 图10.1.1.2 并行通信 简单理解,这两者就是单车道和多车道的概念,同一个时刻,单车道只能通行一辆车,好比串行通信只能传递1位数据;多车道可以通行多辆车好比并行通信传递多位数据。 这两者的对比,我们这里也做了一点归纳,如下图所示。 表10.1.1.1 串行通信和并行通信的对比表 单工/半双工/全双工通信 按数据传输方向分类:单工通信、半双工通信和全双工通信,他们的通信图如下图所示。 图10.1.1.3 3种数据传输方向图 单工通信:数据只能沿一个方向传输 半双工通信:数据可以沿两个方向传输,但需要分时进行 全双工通信:数据可以同时进行双向传输 串行通信属于以上的全双工通信。 同步/异步通信 按数据同步方式分类:同步通信、异步通信,他们的区别如下图所示。 图10.1.1.4 同步和异步通信区别图 同步通信:共用同一时钟信号 异步通信:没有时钟信号,通过在数据信号中加入起始位和停止位等一些同步信号 串行通信属于以上的异步通信。 波特率 双方进行通信,假如不存在时钟信号去同步数据的传输的过程,就需要双方设置一样的数据传输速度,也就是波特率。 在二进制系统中,波特率的含义就是每秒传输多少位,当我们设置波特率为115200,这里的含义就是1秒钟传送9600位(bit)数据。串口通信默认的传输方式除了数据本身8位外,还需要加上起始位和停止位,所以传输1字节数据就需要10位。那1秒钟传输的数据量就是960字节,即一秒钟传输9600位 / 一字节需要传输10位 = 960字节。 电脑的串口助手或者Arduino IDE自带的串口监视器波特率需要跟程序设置的一样,才可以正确接收数据。 10.1.2 UART介绍 UART,Universal asynchronous receiver transmitter,是通用异步接收器/发送器,通常集成在主控器中。UART控制器设有一定容量的数据缓冲区,用于存储通信时的数据。 通常,UART使用两条信号线传输数据,分别为数据发送端TX和数据接收端RX。通信时,一端的数据发送端(TX)连接到另一端的数据接收端(RX),连接形式如下图所示: 图10.1.2.1 UART通信连接形式 在ESP32-S3中,是有3个UART控制器,即UART0、UART1和UART2。3个UART端口对应的引脚如下表所示。 表10.1.2.1 UART端口引脚 上表带有具体IO口是默认使用IO,但是ESP32-S3有IO MUX,所以是可以选择任意GPIO管脚作为UART的引脚。使用Arduino,调用串口初始化函数时,可以指定发送引脚和接收引脚。 ESP32-S3开发板上有一个TYPEC接口是通过一个USB转UART接口芯片连接UART0,使用该串口上传程序或与计算机交互。注意:要识别串口,就得安装串口驱动。 10.1.3 串口相关函数介绍 本小节介绍到的函数可在以下文件中找到: Arduino15\packages\esp32\hardware\esp32\2.0.11\cores\esp32\HardwareSerial.cpp 在HardwareSerial.cpp中已经定义好了三个UART对象Serial、Serial1和Serial2,对应的就是UART0、UART1和UART2,直接使用它们即可。 串口初始化函数介绍在Arduino中,是要使用begin函数初始化串口功能,即 void HardwareSerial::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin, bool invert, unsigned long timeout_ms,uint8_t rxfifo_full_thrhd); 由于参数比较多,所以这里以表格形式说明参数,如下表所示。 参数 | 参数说明 | | | | 串口参数:设置数据位、奇偶校验位和停止位,默认为SERIAL_8N1即数据位8位,不使用奇偶检验,停止位1位 | | | | | | | | 自动检测波特率超时时间,如果超过该时间还没有获得波特率就不会使能串口(若设置baud为0,该参数有意义) | | 接收缓冲区的阈值,当接收器接收到的比阈值多的数据时,产生中断 |
表10.1.3.1 begin函数参数说明表 通常情况下,通过如下语句便可以初始化串口0,默认使用IO43作为串口0的发送引脚,使用IO44作为串口0的接收引脚,8位数据位,无奇偶检验位,1位停止位。 Serial.begin(115200); 当然,我们也可以通过Serial1.begin或Serial2.begin接口去设置串口2和串口3,带上参数既可自由设置发送和接收引脚。 串口发送函数介绍串口初始化完成后,便可以Serial.print、Serial.println和Serial.printf函数向串口助手发送数据。 Serial.print函数用法: Serial.print(val); 其中参数val是要输出的数据,各种类型数据都可以。 Serial.println函数用法: Serial.println(val); Serial.println(val)函数也是使用串口输出数据,不同于Serial.print函数,该函数输出完指定数据后,再输出回车换行符。 下面测试一下这两个函数,在UART例程的loop函数中加入这四句代码。 Serial.print(1); Serial.println("hello world "); Serial.print(2); Serial.println("hello world "); 通过点击Arduino IDE右上角的串口监视器图标打开串口监视器,可以看到如下效果。 图10.1.3.1 print和println函数区别 通过上图可以清楚看到print是直接输出,而println是输出数据后,还要进行换行。 需要注意,串口监视器窗口有一个选择波特率的设置,要选择与程序一样的设置,才能正常发送/接收数据。 Serial.printf函数用法: Serial.printf(char * format, ...); 该函数功能是输出一个字符串,或者按指定格式和数据类型输出若干变量的值,函数返回值为输出字符的个数。 在Serial.printf()函数使用中,会涉及到比较多的格式字符“%d、%c、%f”,“\n、\r”等为转义字符,这里整理了一个表格来说明这些常用的格式字符和转义字符,如下表所示。 表10.1.3.2 常用的格式字符和转义字符 串口接收函数介绍除了输出,串口同样可以接收由串口助手发出的数据。接收串口数据需要使用Serial.read()函数,函数用法: Serial.read(); 调用该函数,每次都会返回1字节数据,该返回值便是当前串口读取到的数据。 下面测试一下这个函数,在UART例程的loop函数中加入这两句代码。 char c = Serial.read(); Serial.print(c); 程序执行情况如下图所示: 图10.1.3.2 串口监视器显示接收到的数据 在图10.1.2.1中圈红框的发送数据框写入“hello_world”回车进行发送,然后在串口监视器界面是可以见到“hello_world”字样,除此之外,还有一些乱码。这些乱码数据是因为没有可读数据造成的。当我们用Serial.print(Serial.read())程序,若没有可读数据,返回的是int型数据-1,对应到char型数据就是乱码。 在使用串口时,Arduino会在SRAM中开辟一段大小为64字节的空间,窗口接收到的数据都会被暂时存放在该空间中,称这个存储空间为缓冲区。当调用Serial.read()函数时,Arduino便会从缓冲区中取出1字节的数据。 通常在使用串口读取数据时,需要搭配使用Serial.available()函数,用法是: Serial.available(); Serial.available函数的返回值为当前缓冲区中接收到的数据字节数。通常该函数会搭配if或者while语句来使用,先检测缓冲区中是否有可读数据,如果有数据,再读取;如果没有数据,跳过读取或等待读取,如下所示。 if (Serial.available() > 0) 或 while (Serial.available() > 0) 下面改进一下上面的代码。 while (Serial.available() > 0) { char c = Serial.read(); Serial.print(c); } 程序执行情况如下图所示: 图10.1.3.3 串口监视器显示接收到的数据 可以看到Serial.available()函数和Serial.read()函数搭配使用下,不会出现乱码现象。 10.2 硬件设计 1. 例程功能 1.回显串口接收到的数据 2.每间隔一定时间,串口发送一段提示信息 2. 硬件资源 1)LED灯 LED-IO1 2)USART0 U0TXD-IO43 U0RXD-IO44 3. 原理图 USB转串口硬件部分的原理图,如下图所示。 图10.3.1 USB转串口原理图 这里需要注意的是:上图中的红色框中的TXD和U0_RXD需要用跳线帽连接,以及RXD和U0_TXD也需要用跳线帽连接。跳线帽连接方法如下图所示。 图10.3.2 跳线帽连接方法 10.3 软件设计 10.3.1 程序流程图 下面看看本实验的程序流程图: 图10.3.1.1 程序流程图 10.3.2 程序解析 1. uart驱动代码这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。UART驱动源码包括两个文件:uart.cpp和uart.h。 下面我们先解析uart.h的程序。在uart头文件中,我们做了下面的引脚定义。 /* 引脚定义 */ /* 串口0默认已经使用了固定IO(GPIO43为U0TXD,GPIO44为U0RXD) * 以下两个宏为串口1或串口2使用到的IO口(例程未使用) */ #define TXD_PIN 19 #define RXD_PIN 20 下面我们再解析uart.cpp的程序,这里只有一个函数uart_init,其定义如下: /** * @param uartx:串口x * @param baud:波特率 * @retval 无 */ void uart_init(uint8_t uartx, uint32_t baud) { if (uartx == 0) { Serial.begin(baud); /* 串口0初始化 */ } else if (uartx == 1) { Serial1.begin(baud, SERIAL_8N1, RXD_PIN, TXD_PIN); /* 串口1初始化 */ } else if (uartx == 2) { Serial2.begin(baud, SERIAL_8N1, RXD_PIN, TXD_PIN); /* 串口2初始化 */ } } 为了方便大家使用不同的串口,所以特定封装了一个uart_init函数。该函数是根据传参去初始化对应串口,而在uart.h中的TXD_PIN和RXD_PIN是专门给串口1或者串口2指定引脚的。初始化串口就是用到Serial库的begin函数。由于Serial库属于系统的核心库,所以使用时不需要导入库的头文件。 2. 04_uart.ino代码在04_uart.ino里面编写如下代码: #include "uart.h" uint32_t chip_id = 0; /* 芯片ID */ /** * @brief 当程序开始执行时,将调用setup()函数,通常用来初始化变量、函数等 * @param 无 * @retval 无 */ void setup() { uart_init(0, 115200); /* 串口0初始化 */ for(int i = 0; i < 17; i = i + 8) { chip_id |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; /* 获取ESP32芯片MAC地址(6Byte),该地址也可作为芯片ID */ } Serial.printf("ESP32 Chip model = %s Rev %d \n", ESP.getChipModel(), ESP.getChipRevision()); /* 打印芯片类型和芯片版本号 */ Serial.printf("This chip has %d cores \n", ESP.getChipCores()); /* 打印芯片内核数 */ Serial.print("Chip ID: "); Serial.println(chip_id); /* 打印芯片ID */ Serial.printf("CpuFreqMHz: %d MHz\n", ESP.getCpuFreqMHz()); /* 打印芯片主频 */ Serial.printf("SdkVersion: %s \n", ESP.getSdkVersion()); /* 打印SDK版本 */ } /** * @brief 循环函数,通常放程序的主体或者需要不断刷新的语句 * @param 无 * @retval 无 */ void loop() { Serial.println("Waitting for Serial Data \n"); /* 等待串口助手发过来的串口数据 */ while (Serial.available() > 0) /* 当串口0接收到数据 */ { Serial.println("Serial Data Available..."); /* 通过串口监视器通知用户 */ String serial_data; /* 存放接收到的串口数据 */ int c = Serial.read(); /* 读取一字节串口数据 */ while (c >= 0) { serial_data += (char)c; /* 存放到serial_data变量中 */ c = Serial.read(); /* 继续读取一字节串口数据 */ } /* 将接收到的信息使用readString()存储于serial_data变量(跟前面4行代码具有同样效果) */ // serial_data = Serial.readString(); Serial.print("Received Serial Data: "); /* 串口监视器输出serial_data内容 */ Serial.println(serial_data); /* 查看serial_data变量的信息 */ } delay(1000); } 在setup函数中,调用uart_init函数对串口0进行初始化。 接下来,在loop函数中,就通过Serial.available函数去查询是否接收到串口数据,假如有接收到,那么就通过Serial.read函数去读取串口数据,把这些串口数据保存到serial_data变量中,最终打印serial_data变量就为串口接收到的数据。程序中提供了两种方式去读取完整串口数据,大家可以自行去选择使用。 10.4 下载验证 下载完之后,打开串口监视器,可以看到不断打印出“Waitting for Serial Data”信息,当我们在消息输入框写入“hello esp32”后,回车发送,“hello esp32”将会回显出来,可以看到如下效果。 图10.4.1 串口发送数据和接收数据
|