ESP32——UART学习笔记
文章目录
本篇目的
- 快速入门ESP32的UART串口。
- 简要介绍UART串口概念,以及ESP32如何通过UART串口与PC通信。
- 如何在ESP-IDF开发环境下对UART串口编程。
适用范围
ESP32全系列(S、C等)芯片和模组。
开发环境
IDF 版本:[ESP-IDF v4.4.3](Release ESP-IDF Release v4.4.3 · espressif/esp-idf (github.com))
例程路径: esp-idf/examples/peripherals/uart at v4.4.3 · espressif/esp-idf (github.com)
操作系统:VMWare, Ubuntu20.04
编辑编译方式:VSCode + Espressif IDF v1.5.1
开发板:[ESP32-S3-DevKitC-1](ESP32-S3-DevKitC-1 - ESP32-S3 - — ESP-IDF 编程指南 v4.4.3 文档 (espressif.com)) *1
开发板供电:带外部供电的 USB HUB
前置知识
-
你已经搭建完了ESP-IDF开发环境。
-
你会复制idf环境中的examples里的例程到你的工作目录,并且能通过命令行成功编译工程。
-
(非必须)你配置好了VSCode的ESP-IDF插件,并且能通过插件编译工程。
-
(非必须,但是能改善体验)通过ESP-IDF插件正确配置工程头文件:
通过VSCode打开工程后,按
ctrl+shitf+p
打开VSCode命令,输入:ESP-IDF: Add vscode configuration folder
然后插件会在工程目录下生成一个名称为
.vscode
的文件夹,打开.vscode/c_cpp_properties.json
,如下图所示:在24行添加:
,
“compileCommands”: “${workspaceFolder}/build/compile_commands.json”
逗号不能少,像这样:
这样你就可以跳转到本工程的其他组件的头文件和源文件了,当然要在你设置了
target
或者编译了之后生成了build
文件夹之后才会起作用。这样你的头文件就不会有波浪线了,函数也能正确跳转:
-
串口
感性认识
-
串口是什么?
感性认识:串行接口(Serial port)简称串口,是一种通信方式和通信接口,它可以让计算机或其他设备与其他设备通过串行数据线进行通信。 串口是一种常用的通信接口,可以在计算机和外部设备之间进行数据传输。 例如,串口可以用来连接打印机、键盘、游戏控制器等设备。
通常,电脑与ESP32之间最常用的通信方式就是一种称为UART的串口,并且只要两根数据线(发送TxD、接收RxD)和一根地线即可双向通信。
图片来源:ESP32-S3 应用程序开发
-
串口的作用?
ESP32通过UART串口与电脑连接时,可以用来输出日志、输出调试信息、传输数据和调试,也可以通过串口控制台例程,像Linux命令行一样控制ESP32。
使用串口作为通信接口的外设中,最常见的就是GPS模块,比如HT1818Z3G5L、Air551G等,ESP32可以通过串口来读取GPS数据或者配置GPS模块。下图是GPS模块HT1818Z3G5L。
定义
串口与并口(并行接口,Parallel Port)相对应,串口实际上并非特指某一种具体的接口(比如UART、SPI、USB等等),串行式逐位传输数据的接口都可以称为串口,但是习惯上串口一般指的是以通用异步收发传输器(Universal Asynchronous Receiver/Transmitter,UART)为控制器的串口。UART的参考资料如下:
本文的串口如无特别说明均指的是UART。
物理接口
如果PC想通过串口与ESP32通信,则需要正确地连接串口线,所以首先需要了解一下串口的物理接口。
串口的物理接口通常指以下两种:
- 单片机上直接引出的,使用TTL电平作为逻辑电平标准的,复用作为串口的引脚。对于ESP32-S3-DevKitC-1来说如下所示,这个只是默认的UART0的串口引脚,对于ESP32-S3来说可以配置任意引脚为串口引脚。
-
由美国电子工业联盟(EIA)制定的串行数据通信的接口标准RS232,它与上一种物理接口最大的不同有两点:
- 一是接口不同,它使用专门的9针的RS232接口(还有25针的RS232接口,但极少使用),如下图所示:
- 二是电平标准不同,TTL的逻辑电平标准用2.4V~5V表示逻辑1,用0V~0.5V表示逻辑0;RS232的逻辑电平标准用**-15V~-3V表示逻辑1**,用**+3V~+15V表示逻辑0**。为什么RS232使用绝对值更高电压作为逻辑标准呢?因为更高的电平可以保障信号在较远距离传输时,不易被其他信号干扰到无法区分逻辑0和逻辑1。
9针的RS232接口有以下引脚(并非按标准顺序排序):
- Transmit Data (TXD):发送数据
- Receive Data (RXD):接收数据
- Data Terminal Ready (DTR):数据终端准备
- Data Set Ready (DSR):数据准备好
- Request To Send (RTS):请求发送
- Clear To Send (CTS):清除发送(发送允许)
- Ring Indicator (RI):响铃指示
- Data Carrier Detect (DCD):数据载波检测
- Ground (GND):地
RS232并非本篇的重点,因此只要记住TxD和RxD分别是用来发送数据和接收数据的就行了,其余引脚DCD、DSR、DTR、RTS、CTS都是用作控流(控制数据流)。其中DTR和RTS通常用作自动下载,RTS和CTS通常用作硬件流控。
TTL电平的串口与RS232电平的串口的关系如下图所示,通常RS232适用于15m以内的串口通信。
在短距离通信(通常<30cm)时,实际上可以不使用RS232的接口标准,直接使用TTL电平进行串口通信。如下所示:
下图为ESP32-S3-DevKitC-1板载UART转USB芯片CP2102。
如何连接
连接示意图如下,需要特别注意的是,在连接串口时,两个设备的TxD和RxD是要交叉连接的,并且TxD只作串行信号输出引脚,单向输出;RxD只作串行信号输入引脚,单向输入。TxD和RxD都是单向的。下图中,ESP32的TxD连接到了CP2102的RxD,ESP32的RxD连接到了CP2102的TxD。当然,完全可以将ESP32的TxD连接到ESP32的RxD自发自收。
由于TxD和RxD是独立的两根线(两个独立的数据通道),完全可以同时发送和接收,这种允许二台设备间同时进行双向数据传输的系统称为全双工(full-duplex)通信系统。与全双工相对,半双工(half-duplex)通信系统只允许二台设备间分时双向数据传输。由于ESP32不支持半双工串口,在此不介绍,如果想要了解半双工串口可以参考以下链接:
stm32 USART串口半双工功能测试_云端FFF的博客-CSDN博客_stm32半双工串口
如果想将全双工串口转为半双工串口,可以参考:如何把双线串口转单线串口? WhyCan Forum(哇酷开发者社区)。
全双工串口转为半双工串口看似有点脱裤子放屁,但是将串口模块的全双工串口转为半双工串口时很有用,特别是在ESP32的IO资源很紧张时。
对于ESP32来说,大部分外设都可以自行指定GPIO,因此可以用一个IO分时复用为仅接收的UART RxD引脚或者仅发送的UART TxD引脚(即单工模式)就可以节省多一个IO。
比如,将GPS模块的串口转为半双工,那么与ESP32连接时可以只连接一个引脚,并且ESP32初始化UART为接收模式即可,需要配置GPS模块时再将那个引脚配置为发送模式。
下图为原理图中的USB转串口部分,在R7和R9附近可以看到交叉连接的串口引脚。
ESP32-S3-DevKitC-1的原理图:sch_esp32-s3-devkitc-1_v1_20210 (espressif.com)
需要注意的是,ESP32使用3.3V电源供电,3.3V在TTL的逻辑电平标准兼容范围之内的,但是ESP32不兼容5V电压,如果你使用的USB转UART是输出的TTL电平是5V的,你需要加上电平转换模块。
UART协议
UART协议对于用户来说并非必须知晓,开发者在使用UART通信时,更多情况下只会更改串口波特率,其余参数均默认,因此将UART协议放在之后的实验中,通过逻辑分析仪讲述。
串口波特率可以不严谨地理解为每秒传输多少个比特(bit)(比特率),比如波特率115200表示每秒传输115200 bit。实际上波特率并不等同于比特率,但是习惯上称呼为波特率。
ESP32的串口
数量
通常一个ESP32有多个UART,不同系列的ESP32(如S系列、C系列)拥有的串口数量也不尽相同,如何知道手上的开发板的串口数量?可以去ESP产品选型器中查看。
参数
通常来说,对于串口,我们最关注的参数差不多就是通信速率了,从ESP32-S3的技术规格书里看到它支持的最高速率可达到5Mbps(如果不算停止位等),这意味着最高速率时,ESP32每秒钟可以传输五百万位(5,000,000 bit/s)数据,也就是六十二万五千字节(625,000 Byte/s = 625 kByte/s),这个速率对于ESP32来说是对外通信的比较快的通信方式,当然在没有丢包重传等保障措施的情况下,为了保证通信质量不会用这么高的速度,最常用的通信速率是115200,下载速率常用460800、921600、1000000、2000000。
其余参数将在程序解读中介绍。
特殊串口
通常,UART0是作为日志输出的串口,UART0引脚对于ESP32-S3-DevKitC-1来说就是丝印标注TX和RX的引脚。如果没特殊需求不需要更改。
对于ESP32-S3来说,USB接口可以虚拟出一个串口,但是使用这个串口运行控制台例程时通常要改程序,而且这个串口会在ESP32开机之后一段时间才会被虚拟出来,因此开机信息可能会丢失开通一部分。
在menucinfig的ESP System Settings中,有这么两个选项:
第一个选项为控制台的输出通道
,默认为UART0,从选项右侧的截图可以看到可以选择自定义串口等通道,帮助内容的翻译如下:
选择发送控制台输出的位置(通过stdout和stderr)。
- 默认是在预定义的GPIO上使用UART0。
- 如果选择 “Custom”,可以选择UART0或UART1,并且可以选择任何引脚。
- 如果选择 “无”,除了ROM启动器的初始输出外,任何UART上都不会有控制台输出。这个ROM输出可以通过GPIO捆绑或EFUSE来抑制,详情请参考芯片数据手册。
- 在带有USB OTG外设的芯片上,"USB CDC "选项将输出重定向到CDC端口。这个选项使用芯片ROM中的CDC驱动。这个选项与TinyUSB协议栈不兼容。
- 在带有USB串行/JTAG调试控制器的芯片上,选择该选项可以将输出重定向到该设备的CDC/ACM(串行端口仿真)组件。
第二个选项为控制台次要输出通道
,默认选择USB串口,也可以关闭次要控制台,帮助内容的翻译如下:
当UART0端口作为主端口被选中但没有连接时,这个二级选项支持通过其他特定端口输出,如USB_SERIAL_JTAG。这个二级输出目前只支持不使用REPL的非阻塞模式。如果你想用REPL在阻塞模式下输出或通过这个次级端口输入,请在 "控制台输出通道"菜单中把主配置改为这个端口。
这意味这ESP32通过ESP_LOGI
输出的日志内容会向USB串口复制一份,通过USB串口也可以查看日志。ESP_LOGI
是ESP-IDF中输出信息级别日志的函数,下文的实验中将会用到。
ESP32上的串口实验
从以上内容中,已经了解了串口的基本概念、串口的连接方式、ESP32-S3串口的信息、常用的参数和特殊的串口,接下来会通过具体程序来解释UART协议,以及如何对串口编程。
接下来主要讲解以下例程:
- esp-idf/examples/peripherals/uart/uart_echo 在 v4.4.3 ·乐鑫/ESP-IDF (github.com)
esp-idf/examples/peripherals/uart/uart_async_rxtxtasks v4.4.3 ·乐鑫/ESP-IDF (github.com)esp-idf/examples/peripherals/uart/uart_events v4.4.3 ·乐鑫/ESP-IDF (github.com)esp-idf/examples/peripherals/uart/uart_repl v4.4.3 ·乐鑫/ESP-IDF (github.com)
如何使用例程?在ESP-IDF编程指南中的第五步:开始创建工程
中有详细的步骤,在此不再赘述。
快速入门 - ESP32-S3 - — ESP-IDF 编程指南 v4.4.3 文档 (espressif.com)
如何配置VSCode上的ESP-IDF插件:ESP32-IDF环境搭建之vscode环境
开始实验前的准备
你需要准备以下材料:
-
ESP32开发板,这里是ESP32-S3-DevKitC-1 *1
-
microUSB数据线(根据你的开发板选择) *2
-
如果你的ESP32开发板没有板载USB转串口,你需要准备一个USB转串口模块,并按以下方式连接:
- 如果你的ESP32开发板有板载USB转串口,用数据线连接标注的UART的接口并插入电脑即可。
uart_echo
复制该例程到你的路径,该例程的路径是:你的esp-idf路径/examples/peripherals/uart/uart_echo
。
通过VSCode打开该例程,如有需要,按前置知识
的方法配置好VSCode的IntelliSense。
例程说明
如README.md所说,该例程是串口回声例程,其实就是复读机的意思,你通过UART向ESP32发送什么,ESP32就会原封不动地发回来。考虑到有些人可能没有多的USB转串口模块,因此接下来会将例程中默认的串口改为串口0,这样就可以不用别的USB转串口模块了,因此可以不完全按照README.md中的操作。
This example demonstrates how to utilize UART interfaces by echoing back to the sender any data received on configured UART.
这个例子演示了如何利用UART接口,将配置的UART上收到的任何数据回传给发送方。
运行它
- 打开路径,通过
ls
命令查看路径下的内容:
- 获取你的
esp-idf
环境变量,我设置为:get_idf4.4.3
- 设置目标,我使用的ESP32-S3开发板,因此设为
esp32s3
:idf.py set-target esp32s3
- (可省略)打开menuconfig:
idf.py menuconfig
打开倒数第4个选项Echo Example Configuration
就可以查看工程中有什么选项可以设置:
在这里并不需要对这些配置进行什么更改,因为在menuconfig中的任何更改都会导致程序全部重新编译,编译一千多个文件还是挺麻烦的,所以在需要频繁更改选项时还是建议用.h
设置选项。
Echo Example Configuration中选项的含义分别是:
- UART port number:该例程的串口号,既待会运行时通过哪个串口来复读。在下面的步骤中将改成UART0。
- UART communication speed:串口的波特率,默认115200即可。
- UART RXD pin number:UART接收引脚。
- UART TXD pin number:UART发送引脚。
- UART echo example task stack size:该例程创建的任务的栈的大小,如果使用了
ESP_LOGI
,建议最少也要2048字节(ESP-IDF的freeRTOS创建任务时栈的单位是字节)。
了解示例的选项含义后即可退出,在menuconfig中下方有提示,按Q
退出,按S
保存。
如何查看例程有什么可以设置的选项呢?
一般查看项目的
Kconfig
文件就知道了,在这里是:main/Kconfig.projbuild
.第一行
menu
后面跟着的就是菜单名称。
-
打开
main/uart_echo_example_main.c
:我们用以下内容替换29行到36行的宏定义部分:
#define ECHO_TEST_TXD (UART_PIN_NO_CHANGE) /* CONFIG_EXAMPLE_UART_TXD */
#define ECHO_TEST_RXD (UART_PIN_NO_CHANGE) /* CONFIG_EXAMPLE_UART_RXD */
#define ECHO_TEST_RTS (UART_PIN_NO_CHANGE)
#define ECHO_TEST_CTS (UART_PIN_NO_CHANGE)
#define ECHO_UART_PORT_NUM (UART_NUM_0) /* CONFIG_EXAMPLE_UART_PORT_NUM */
#define ECHO_UART_BAUD_RATE (115200) /* CONFIG_EXAMPLE_UART_BAUD_RATE */
#define ECHO_TASK_STACK_SIZE (4096) /* CONFIG_EXAMPLE_TASK_STACK_SIZE */
这里其实就是将menuconfig的设置用自己的设置替换了而已,注意34行,我们使用默认的串口UART0,UART0的引脚默认就是原理图上的引脚,因此不需要改变。
UART_PIN_NO_CHANGE是该引脚不需要改变的意思,它在
你的ESP-IDF路径/components/driver/include/driver/uart.h
中有定义。
- 编译
idf.py build
:
正在编译中:
编译完成!
-
烧录
idf.py -p /dev/ttyUSB0 flash
:其中的
/dev/ttyUSB0
需要根据你的串口修改。与 ESP32-S3 创建串口连接 - ESP32-S3 - — ESP-IDF 编程指南 v4.4.3 文档 (espressif.com)
烧录完成!
-
运行串口监视器,看看效果
idf.py -p /dev/ttyUSB0 monitor
:其中的
/dev/ttyUSB0
需要根据你的串口修改。看起来没什么大不了的,试着输入一些字符:
可以看到,灰色的是“我输入的字符”,后面绿色的字符是ESP32接收到了我发送的字符,并通过
ESP_LOGI
函数发回来的信息,Recv str:
后面则跟着“我输入的字符”。这个“我输入的字符”其实并不是我真正输入的,我输入的字符其实是看不见的,只是ESP32原封不动输出了一遍了而已。接下来解释程序时会解释这是什么意思。 -
退出串口监视器:直接按
ctrl+l
即可退出。如果你对串口监视器不熟悉,请参考:IDF 监视器 - ESP32-S3 - — ESP-IDF 编程指南 v4.4.3 文档 (espressif.com)
-
至此,你已经成功运行了这个例程!
它是怎么运行的?
接下来,我将带你了解他是如何运行的。同时,根据我的步骤,你也可以了解如何去学习esp-idf中的例程。
1. 先读README.md
一个好的README.md会交代例程的基本信息、作用、如何使用等信息,因此,阅读README.md是十分重要的。如果英语不太好也没关系,可以复制其中的内容到翻译软件,或者直接打开该例程的网页esp-idf/examples/peripherals/uart/uart_echo 在 v4.4.3 ·乐鑫/ESP-IDF (github.com),右键翻译该网页即可。
考虑到有些人只有一个开发板和一个数据线,因此没有完全按照例程中README的需求使用UART2和USB转串口模块,经过运行它
中的步骤,只用板载的USB转串口和UART0即可。
2. 阅读源码
打开main/uart_echo_example_main.c
:
esp-idf中,程序的入口是app_main
,比较类似于STM32中的main
。因此需要先看app_main
。
在app_main
中,可以看到他创建了一个任务,来运行复读机的程序,除此之外没有别的操作。如果你原意,完全可以将echo_task
的内容全部放到app_main
中。
如果你对freeRTOS创建任务不太熟悉,可以参考:xTaskCreate() - 追风*逐浪 - 博客园 (cnblogs.com)
echo_task
在上文定义了,跳转到echo_task
:
先忽略uart_config_t uart_config
,仅看while(1)之前的主要进行的操作,也就是这3行:
ESP_ERROR_CHECK(uart_driver_install(ECHO_UART_PORT_NUM, BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags));
ESP_ERROR_CHECK(uart_param_config(ECHO_UART_PORT_NUM, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(ECHO_UART_PORT_NUM, ECHO_TEST_TXD, ECHO_TEST_RXD, ECHO_TEST_RTS, ECHO_TEST_CTS));
这三行做了什么呢?
首先是ESP_ERROR_CHECK
,顾名思义,就是用来检查错误的,他定义在esp_err.h
,如果保持默认设置,ESP_ERROR_CHECK
中检测到了返回了代表错误的值ESP_FAIL
,他会输出错误的信息,并重启ESP32,如下图所示。61行的ESP_ERROR_CHECK(ESP_FAIL);
是用来测试效果的,正常情况不需要加上去。
uart_driver_install
还是顾名思义,uart_driver_install
,也就是安装串口驱动,这种类似Linux开发的风格也是ESP-IDF的特色之一。如果你配置好了VSCode,你也可以直接参看uart_driver_install
的声明:
声明中已经讲的很清楚了,不懂的地方可以复制到翻译器看一下,在此给出uart_driver_install
的简要用法:
esp_err_t uart_driver_install(
/* 你需要安装驱动的串口 */
uart_port_t uart_num,
/* Rx环形缓冲区的大小,一般设置为接收数据的缓冲区的两倍 */
int rx_buffer_size,
/* Tx环形缓冲区的大小,如果为0,那么发送数据时函数会被阻塞,直到发送完成 */
int tx_buffer_size,
/* UART事件队列的大小/深度,在例程中uart_events解释 */
int queue_size,
/* UART事件队列句柄,在例程中uart_events解释 */
QueueHandle_t *uart_queue,
/* 中断标志,通常设为0,使用默认的中断优先级即可 */
int intr_alloc_flags);
在例程中,就是:
uart_driver_install(
ECHO_UART_PORT_NUM, /* 安装串口0的驱动,UART_NUM_0 */
BUF_SIZE * 2, /* Rx环形缓冲区的大小,1024*2(字节) */
0, /* 没有设置Tx环形缓冲区,这意味着我们发送数据时会阻塞到发送函数中 */
0, /* 没有设置UART事件队列 */
NULL, /* 没有设置UART事件队列句柄,我们没有启用这个功能 */
intr_alloc_flags /* 在这里是0,即默认中断优先级 */
);
需要注意,CONFIG_UART_ISR_IN_IRAM是在menuconfig中的一个选项,如果启用这个选项,会将串口中断服务函数放到IRAM中。
#if CONFIG_UART_ISR_IN_IRAM
intr_alloc_flags = ESP_INTR_FLAG_IRAM;
#endif
什么是IRAM?存储器类型 - ESP32-S3 - — ESP-IDF 编程指南 v4.4.3 文档 (espressif.com)
如何查找这个选项呢?打开menuconfig,根据menuconfig下方的提示,按/
即可查找符号:
输入UART_ISR_IN_IRAM
(注意不要输入CONFIG_
):
回车即可查看这个选择的位置:
在英文输入法下,按shitf+/
,其实就是?
,即可获得帮助信息:
If this option is not selected, UART interrupt will be disabled for a long time and may cause data lost when doing spi flash operation.
如果不选择该选项,在进行spi flash操作时,UART中断将长时间被禁用,可能导致数据丢失。
通过uart_driver_install
,已经初步设置了UART0的基本参数。
uart_param_config
uart_param_config
是设置UART配置参数的意思,由于我们还没有了解UART协议,此处暂且略过UART参数结构体,即上文提到的uart_config_t uart_config
。
esp_err_t uart_param_config(
/* 你需要配置的串口 */
uart_port_t uart_num,
/* 指向UART参数结构体的指针 */
const uart_config_t *uart_config);
uart_set_pin
uart_set_pin
用于设置引脚,对于ESP32-S3来说可以设置任意引脚,ESP32需要注意有些引脚只能设置为输入模式。
esp_err_t uart_set_pin(
uart_port_t uart_num, /* 串口号 */
int tx_io_num, /* 发送引脚TxD */
int rx_io_num, /* 接收引脚RxD */
int rts_io_num, /* 请求发送引脚RTS */
int cts_io_num /* 清除发送引脚CTS */
);
在这里,每个引脚都是宏定义为了UART_PIN_NO_CHANGE
,也就是未改变,那么怎么查看具体是哪个引脚呢?可以在ESP32-S3的数据手册中的2.5 GPIO 功能
找到答案:
U0代表UART0,也就是:
串口引脚 | GPIO号 |
---|---|
U0TXD | 43 |
U0RXD | 44 |
U0RTS | 15 |
U0CTS | 16 |
while循环
在whlie循环之前还申请了一段空间用于存放数据:
while循环的主体如下:
while (1) {
// Read data from the UART
int len = uart_read_bytes(ECHO_UART_PORT_NUM, data, (BUF_SIZE - 1), 20 / portTICK_RATE_MS);
// Write data back to the UART
uart_write_bytes(ECHO_UART_PORT_NUM, (const char *)data, len);
if (len) {
data[len] = '\0';
ESP_LOGI(TAG, "Recv str: %s", (char *)data);
}
}
首先通过uart_read_bytes
函数读取串口ECHO_UART_PORT_NUM
的信息,也就是读取UART0的信息。这个函数的参数的含义如下:
uart_read_bytes(
ECHO_UART_PORT_NUM, /* 串口端口号,这里是串口0 */
data, /* 指向缓冲区的指针 */
(BUF_SIZE - 1), /* 最大的读取的数据长度,设置为(BUF_SIZE - 1)是因为需要\0进行结尾 */
20 / portTICK_RATE_MS /* 超时时间,注意这里是RTOS ticks数 */
);
这里的uart_read_bytes
的含义是读取UART0中不超过1023字节的数据到数据缓冲区data
,如果超过20ms没有数据那么就返回读取到的数据的长度。
然后通过uart_write_bytes
函数把刚刚读取到的数据写回去:
uart_write_bytes
的声明如下:
uart_write_bytes(
ECHO_UART_PORT_NUM, /* 串口端口号,这里是串口0 */
(const char *)data, /* 指向待发送的缓冲区的指针 */
len /* 数据长度 */
);
这里的uart_write_bytes
的含义是从数据缓冲区data
向UART0写长度为len
字节数据,由于之前设置了Tx环形缓冲区的大小为0,所以它会一直阻塞到这里发送数据,直到数据发送完毕为止。
在实验中,也就是我们按下按键后,灰色的字节就是通过uart_write_bytes
发送的数据,这也是“我输入的字符”加双引号的原因,因为这个是ESP32发回来的字符。
if(len)
的作用是,如果接收到了数据(len不等于0),那么将数据的最后一个字符设为字符串结尾data[len] = '\0';
:(因为没有清空字符串。如果不加,上一次发送了hello
,这一次发送了123
,那么这一次就会输出123lo
,即包含上一次的字符,这不是我们想要的结果)
ESP_LOGI
ESP_LOGI
是一个非常重要也是非常常用的函数,它的作用是输出信息级别的日志,它在esp_log.h
中声明,如何使用它呢?首先包含头文件#include "esp_log.h"
,然后定义一个TAG
,用于说明这个日志是在哪里输出的:
然后像printf
一样使用就行了,在这里是:
ESP_LOGI(TAG, "Recv str: %s", (char *)data);
它输出的效果是这样的:
I
表示这个是信息(Information)级别的日志,括号内是开机到输出这个信息的毫秒数,UART TEST
就是我们刚刚定义的TAG
,冒号后的内容就是像printf
一样使用时应该输出的结果。绿色的原因是它增加了颜色的转义字符。
使用ESP_LOGI
时不需要初始化,这是因为在启动阶段系统已经初始化过了。虽然默认是通过UART0输出,但是不代表你使用UART0时不用初始化UART0(手动狗头)。
在这里它的含义是,将接收到的内容通过日志ESP_LOGI
输出。
如果想要了解更多日志库的内容,可以参考:ESP32学习笔记(6)——Log日志库使用 - 简书 (jianshu.com)
至此,除了UART协议和uart_config_t uart_config
没有说明,你已经大概了解了如何使用这个例程。
通过逻辑分析仪解释UART协议
之所以要将UART协议放到最后再说,是因为大部分情况下都不需要使用上UART协议的具体内容,就把UART当黑盒用,知道输入输出即可。虽然UART协议用起来和学起来是比较简单和容易的,但是如果换稍微复杂一点的协议,比如SPI只看协议时序图就比较难理解了。而且结合具体波形更能理解使用的过程中发生了什么,怎么发生的。废话少说,上逻辑分析仪:
烧录程序后,打开逻辑分析仪,输入一个字母Z
查看波形;
波形长这样,接下来简要介绍一下各个部分的含义:
如何理解这个波形呢?我们需要从UART时序图入手,下图为UART数据帧的比较完整的时序图:
从UART数据帧中,可以看到它由以下部分组成:
-
起始位:如果没传输数据,那么UART数据线会保持高电平状态,如果数据线从高电平被拉低到了低电平,这意味这需要传输数据了,低电平维持的时间为1个时钟周期(1 bit)。
-
数据位:也就是UART数据帧中实际的数据位,数据位的长度可以是5到8位,默认为8位。这里也是8位数据位。通常,数据是低位优先发送的,在这里,
Z
的ASCII码值是0x5A,换成二进制是0b01011010,由于发送时是低位在前发送,所以发送的顺序是01011010
,好像没什么变化。
换W
效果会更明显,W
的ASCII码值是0x57,换成二进制是0b01010111,发送的顺序是11101010
:
-
奇偶校验位:
默认的设置是不使用就校验位的,因此此处没有体现出来。
当UART控制器启用了奇偶校验,接收到数据位时会对数据帧中的
1
进行计数,如果接收到的是偶数的数据位,那么计数结果为0;如果是奇数,那么计数结果为1。在数据没有出错的情况下,奇偶校验位结果等同与计数结果;反之,则说明数据位的数据发生了改变。
当然,奇偶校验作为一种十分简单的校验方法并不适用于所有情况,比如如果数据位中同时有两个相同电平的数据位发生了改变,奇偶校验是检测不出来的。
-
停止位:表示数据位传输结束,通常会保持1到2个bit的时间。
了解了UART帧的结构,让我们看看UART帧和我们上文略过的串口配置结构体uart_config_t uart_config
的关系,该结构体的声明如下:
typedef struct {
int baud_rate; /*!< UART 波特率*/
uart_word_length_t data_bits; /*!< UART 数据位长度,5~8位,通常8位 */
uart_parity_t parity; /*!< UART 校验模式,通常不校验 */
uart_stop_bits_t stop_bits; /*!< UART 停止位长度,1~2位,通常1位 */
uart_hw_flowcontrol_t flow_ctrl; /*!< UART 硬件控流模式,通常不启用 */
uint8_t rx_flow_ctrl_thresh; /*!< UART HW RTS阈值,通常不使用 */
union {
uart_sclk_t source_clk; /*!< UART 时钟源 */
bool use_ref_tick __attribute__((deprecated)); /*!< 忽略即可 */
};
} uart_config_t;
下图显示了可以填进结构体参数的枚举。
在我们的例程中是这样配置的:
uart_config_t uart_config = {
/* 波特率设置为115200 */
.baud_rate = ECHO_UART_BAUD_RATE,
/* 8位数据位 */
.data_bits = UART_DATA_8_BITS,
/* 不使用奇偶校验 */
.parity = UART_PARITY_DISABLE,
/* 1位停止位 */
.stop_bits = UART_STOP_BITS_1,
/* 不使用硬件流控 */
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
/* 如无特别需求默认使用UART_SCLK_APB即可 */
.source_clk = UART_SCLK_APB,
};
与逻辑分析仪的波形对比:
可以看到他们之间的对应关系。
自此,uart_echo例程已经基本梳理了一遍了。
3. 试着修改参数
你已经大概了解了UART协议,了解了uart_config_t中的参数和UART波形的关系,为了加深理解,你可以试着修改参数,比如:
- 将8位数据位改为7位UART_DATA_7_BITS,再用其他串口上位机修改对应的参数看看能否正常通信?
- 修改ESP32的程序的奇偶校验为奇校验,串口上位机设置为偶校验,看看能否接收到数据?
- 修改波特率,看看最高能到多快还能正常通信?
- 修改停止位,比如改成1.5位停止位,看看发送单个字符和连续发送两个字符时会发生什么?
- 20ms的超时时间对人来说有点快,试着修改超时时间,设置多大才能连续输入字符?
- 灰色字符如何换行,不与绿色的LOG信息在同一行?
- ……