当前位置: 首页 > news >正文

ESP-IDF+vscode开发ESP32第三讲——UART

目录

前言

一、ESP32中UART的配置

1.1 rs485

1.2 IrDA

1.3 流控

硬件流控

软件流控

1.4 GDMA

二、代码编写

2.1 uart.c

2.2 uart.h

2.3 mian.c

2.4 重点提示

三、结果展示


前言

本文基于第一章创建好的工程,来写一个基于UART的通信工程,本章不对UART这个通讯技术本身进行讲解,在STM32篇已经涉及到了。重点在于梳理UART在ESP32上的的衍生功能

开发板芯片是ESP32-P4、ESP-IDF版本是5.5.3。


一、ESP32中UART的配置

ESP32-P4芯片中共有六个 UART 控制器,包含五个正常功能的 UART0~UART4 和一个满足低功耗需求的 LP UART。另外,UART 还可以用于红外数据交换 (IrDA) 或 RS485 调制解调器。一些基本特性如下:

注意,ESP32和STM32不一样,它的UART不是事先规定复用到哪些特殊引脚上,而是可配置使用任意 GPIO 管脚,也就是说你可以选择任何一个GPIOx引脚成为RXD或TXD引脚,当然有些特殊引脚除外。

当然,官方也事先给一些引脚配置了默认复用功能。以ESP32-P4为例,如下:

接口功能功能IO默认IO
JTAG接口用于调试、烧录代码

MTCK
MTDI
MTMS

MTDO

GPIO2
GPIO3

GPIO4

GPIO5

USB1P1_N/P当不使用上述JTAG接口,可用该接口代替JTAG接口的功能

USB1P1_N0
USB1P1_P0

USB1P1_N1
USB1P1_P1

GPIO24

GPIO25

GPIO26

GPIO27

UART0异步通信串口

UART0_TXD

UART0_RXD

GPIO37

GPIO38

LP_UART低功耗异步通信串口

LP_UART_TXD

LP_UART_RXD

LP_GPIO14

LP_GPIO15

USB专用USB 2.0 接口

USB D-

USB D+

USB_DM

USB_DP

以上是常用的,实际中还有更多,这些是我们比较关注的,平时优先保证这些引脚的默认复用功能。

1.1 rs485

ESP32的UART的五个 UART 控制器支持 RS485 通讯模式,这是stm32所不具备的。RS485 有两线半双工及四线全双工两种选择,UART 模块采用两线半双工模式,也就是只要两根差分信号线,只能收或者发。控制结构图如下:

当DE被使能为1时,就使能驱动器D了,此时使用了RS485 发送模式,使用D+、D-差分信号发送数据,数据内容由TXD决定;当DE被使能为0时,就关闭驱动器D了,此时使用原本UART通讯模式,直接使用TXD发送数据。

当RE被使能为0时,就使能接收器R了,此时使用了RS485 接收模式,使用D+、D-差分信号接收数据,数据内容转化为RXD;当DE被使能为1时,就关闭接收器R了,此时使用原本UART通讯模式,直接使用RXD接收数据。

在其他的MCU中,如STM32,没有内置RS485控制器,也就是图中右半部分,此时想要使用RS485功能需要外置RS485控制器。当然ESP32也可以不使用内置的RS485控制器,改为外置的。

1.2 IrDA

IrDA 是 UART 的红外无线版,它的核心作用是让普通串口通过红外光无线通信,且上层软件完全不用改,只改硬件物理层。家里的电视 / 空调遥控器用的就是IrDA协议。

对比维度普通 UART(有线串口)IrDA UART(红外串口)
传输介质金属导线(TX/RX/GND)红外光(940nm 红外 LED)
信号形式直接电平信号(高 / 低电平)脉冲编码信号(硬件自动转脉冲)
通信模式全双工(可同时收发)半双工(同一时间只能发 OR 收)
硬件结构仅 UART 控制器,直连导线UART +红外收发模块(发射管 + 接收头)
电气隔离无隔离,需要共地完全电气隔离,无需共地
传输距离有线 1~5 米红外 0~3 米(必须对准)

ESP32的UART 实现了其物理层协议。在 IrDA 编码模式下,支持最大信号速率到 115.2 Kbit/s,即 SIR 模式。来看一下这个模式下两种串口的时序对应,需要使用时再去深入研究

ESP32的UART的五个 UART 控制器也支持 IrDA 通讯模式,IrDA 是半双工传输协议,结构图如下

置位 UART_IRDA_EN 使能 IrDA 功能。置位 UART_IRDA_TX_EN(置 1)使能 IrDA 发送数据,这时不允许 IrDA 接收数据;复位 UART_IRDA_TX_EN(清 0) 使能 IrDA 接收数据,这时不允许 IrDA 发送数据。

1.3 流控

UART 流控是一种防止发送方发得太快、接收方缓存爆掉丢数据的控制机制,UART的流控分为硬件流控和软件流控。

硬件流控

靠 2 根额外硬件信号线实现:RTS / CTS

  • CTS (Clear To Send) 输入信号,清除发送,低电平有效
  • RTS (Request To Send) 输出信号,请求发送,低电平有效
  • 双方CTS接RTS,RTS接CTS,交叉连接。

工作原理如下:

当接收方 RX 缓存快满会拉低 RTS,RTS输出低电平,发送方检测到CTS变低,立即停止发送;等待接收方数据处理完毕,缓存空余,拉高RTX,RTS输出高电平,发送方检测到CTS变高,立即继续发送。空闲时CTS和RTS均上拉。

硬件流控:速度快、实时性强,不干扰数据内容,稳定可靠,工业常用。

ESP32的UART硬件流控主要通过输出信号 rtsn_out 以及输入信号 ctsn_in 进行数据流控制。信号连接图如下:

软件流控

不占任何额外引脚,直接在串口数据里插入特殊控制字符

  • XOFF = 0x13→ 暂停发送
  • XON = 0x11→ 恢复发送

工作原理如下:

当接收方 RX 缓存快满会主动发一个xoff,发送方检测到xoff字节,立即停止发送;等待接收方数据处理完毕,缓存空余会主动发一个xon,发送方检测到xon字节,立即继续发送。

软件流控:不占 GPIO,省引脚,但反应慢,依赖数据链路延迟,高波特率下容易丢包,且不能传二进制数据(因为如果数据里恰好有 0x11/0x13 会被误判)

1.4 GDMA

GDMA 是 ESP32 系列芯片中的通用直接内存访问控制器,核心作用是在 CPU 不参与的情况下,让外设与内存、内存与内存之间直接高速传输数据,从而减轻 CPU 负载、提升系统整体性能,尤其适合大批量数据传输场景。

ESP32-P4 中的五个 UART 接口通过通用主机控制器接口 (UHCI) 共用 1 组 GDMA TX/RX 通道。这代表了同时只能有一个uart接口使用GDMA通道。在 GDMA 模式下,支持对 HCI 协议数据包的解析 (decoder) 及数据包封装 (encoder)。数据传输图如下:

在 GDMA Rx 通道接收数据前,软件将接收链表准备好。 通用主机控制器接口 (UHCI) 会将 UART 接收到的数据传送给 decoder。经过 decoder 解析之后的数据在 GDMA 通道的控制下存入接收链表指定的 RAM 空间。

官方手册在uart章节并没有涉及到GDMA模式的使用,等后面涉及到了,在进行补充。

二、代码编写

这里只使用uart的基本模式。基于工程模板创建组件uart。添加依赖:

  • REQUIRES esp_driver_uart
  • REQUIRES esp_driver_gpio

驱动代码如下,我这里始终使用GPIO37和GPIO38作为uart串口,是因为我的开发板只有这个引脚接入了空闲的usb接口,当然可以使用其他引脚,不过不好接线读取。可以在0-4中任意改变uaer端口。

2.1 uart.c

#include <stdio.h> #include "uart_m.h" #include "driver/gpio.h" #include "driver/uart.h" static const char *TAG = "UART"; QueueHandle_t uart_intr_handle= NULL; void uart_intr_callback(void *arg); int uart_band_aotuset(void) { uart_bitrate_detect_config_t uart_bitrate_config = { .rx_io_num = uart_rx_pin, .source_clk = UART_SCLK_XTAL, }; ESP_ERROR_CHECK(uart_detect_bitrate_start(uart_port, &uart_bitrate_config)); vTaskDelay(pdMS_TO_TICKS(20)); uart_bitrate_res_t res; ESP_ERROR_CHECK(uart_detect_bitrate_stop(uart_port, true, &res)); if(res.low_period == 0 || res.high_period == 0){ ESP_LOGE(TAG, "检测失败:无有效边沿"); return ESP_FAIL; } else{ ESP_LOGI(TAG, "检测成功,波特率为:%d", res.clk_freq_hz / (res.low_period + res.high_period)); return res.clk_freq_hz / (res.low_period + res.high_period); } } void uart_init(void) { uart_config_t uart_config = { .baud_rate = 115200, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .source_clk = UART_SCLK_DEFAULT, }; ESP_ERROR_CHECK(uart_driver_install(uart_port, uaer_rx_buffer, uart_tx_buffer, 5, &uart_intr_handle, 0)); ESP_ERROR_CHECK(uart_param_config(uart_port, &uart_config)); ESP_ERROR_CHECK(uart_set_pin(uart_port, uart_tx_pin, uart_rx_pin, -1, -1, -1, -1)); ESP_ERROR_CHECK(uart_set_mode(uart_port, UART_MODE_UART)); ESP_LOGI(TAG, uart_is_driver_installed(uart_port)? "UART driver is installed" : "UART driver is not installed"); //中断配置 uart_intr_config_t intr_config = { .intr_enable_mask = uart_intr, .rx_timeout_thresh = 20, .rxfifo_full_thresh = 50, }; ESP_ERROR_CHECK(uart_intr_config(uart_port, &intr_config)); uart_enable_intr_mask(uart_port, uart_intr); xTaskCreate(uart_intr_callback, "uart_intr", 4096, NULL, 5, NULL); ESP_LOGI(TAG, "UART初始化成功,波特率为:%d", uart_config.baud_rate); } void uart_intr_callback(void *arg) { uart_event_t uart_event; size_t wait_read_size; int len; char read_data[100]; while(1) { xQueueReceive(uart_intr_handle, (void*)&uart_event, portMAX_DELAY); switch (uart_event.type) { case UART_DATA: if (uart_event.timeout_flag == false){ ESP_LOGI(TAG, "触发接收FIFO阈值中断, uart rx data size: %d", uart_event.size); } else{ ESP_LOGI(TAG, "触发接收超时中断, uart rx data size: %d", uart_event.size); } break; default: ESP_LOGI(TAG, "其他未知事件触发"); break; } uart_get_buffered_data_len(uart_port, &wait_read_size); len = uart_read_bytes(uart_port, read_data, wait_read_size, 1000); if(len > 0){ read_data[len] = '\0'; ESP_LOGI(TAG, "uart rx data: %s", read_data); } len = uart_write_bytes(uart_port, read_data, len); ESP_LOGI(TAG, "uart tx length: %d", len); } }

2.2 uart.h

#ifndef __UART_H_ #define __UART_H_ #include "driver/gpio.h" #include "driver/uart.h" #include "hal/uart_ll.h" // 取得uart的寄存器地址 #define uaer_rx_buffer 512 #define uart_tx_buffer 512 #define uart_port UART_NUM_4 #define uart_tx_pin GPIO_NUM_37 #define uart_rx_pin GPIO_NUM_38 #define uart_intr UART_INTR_RXFIFO_TOUT | UART_INTR_RXFIFO_FULL void uart_init(void); // 初始化uart int uart_band_aotuset(void); // 检测波特率 #endif

2.3 mian.c

#include <stdio.h> #include "user.h" #include "uart.h" #include "esp_log.h" static char* TAG = "MAIN"; void app_main(void) { CONSOLE_REPL_INIT(); // 初始化控制台REPL环境 uart_init(); while (1) { char *test_str = "This is a test string.\n"; uart_write_bytes(uart_port, test_str, strlen(test_str)); vTaskDelay(pdMS_TO_TICKS(3000)); } }

代码流程如上,下面会对一些重点进行提示。具体函数功能可以参考官方的编程手册和我整理的《ESP32 实用API指南》

2.4 重点提示

UART 驱动本身没有像其他组件那样有注册回调函数API,所有的中断事件都会存入队列,函数uart_driver_installqueue_size就是用来设置这个中断队列的长度,就是最大能容下几个中断源。uart_queue返回的是中断队列的句柄。

uart_intr_configuart_enable_intr_mask需要配套使用,一个用于配置中断源,一个用于使能中断源。

需要创建一个任务专门用于处理uart数据接收和发送,任务栈建议4096起步,否则容易溢出。

声明#include "hal/uart_ll.h"能够使用中断源的宏,否则会报错。具体中断源见ESP32-P4 技术参考手册> UART 控制器 (UART) > UART 中断 [PDF]。

比较常用的中断源是UART_INTR_RXFIFO_TOUTUART_INTR_RXFIFO_FULL,TOUL是空闲中断,一个数据包接收后产生中断;FULL是接收阈值中断,当一个包的字节数大于设定的阈值便产生中断。

在中断任务中使用队列接收函数xQueueReceive,阻塞等待中断源的到来,将接收的缓冲区指针用结构体uart_event_t接收,结构体中uart_event_type_t用来判断中断类型。其中上面说的TOUL和FULL两个中断都会归于类型UART_DATA,其他则是一个类型对应一个中断。

结构体uart_event_t中的timeout_flag用来判断类型UART_DATA具体是哪一个中断源触发的,当timeout_flag = false代表是FULL中断,timeout_flag = ture代表是TOUL中断。另外size用来判断类型UART_DATA接收到的数据大小。

函数uart_band_aotuset用来实现硬件自动波特率检测,我这里并未使用。

三、中断掩码含义

ESP32 的UART中断共有19个,为了方便使用,这里进行分类说明。系统有两个文件定义了中断标志位的宏,虽然含义相同,但是宏的命名方式有些区别。

分别是来自于系统组件《uart_reg.h》的 UART_XXXX_INT_ENA和来自于系统组件《uart_ll.h》的UART_INTR_XXXX。

我发现官方API,例如uart_disable_rx_intr使用的是《uart_ll.h》的宏,那我们这保持统一,也用文件《uart_ll.h》的宏。

1、基础收发中断

这是日常开发中最常用的中断,用于管理数据的接收和发送。

UART_INTR_RXFIFO_FULL(接收 FIFO 满中断)

  • 含义:当接收 FIFO(硬件缓存)中的数据量达到或超过你设定的阈值(如 120 字节)时触发。

  • 场景:用于批量读取数据。当数据积攒到一定数量后,触发中断让 CPU 一次性读走,减少中断频率,提高效率。

UART_INTR_RXFIFO_TOUT(接收 FIFO 超时中断)

  • 含义:接收 FIFO 中有数据,但在设定的超时时间内没有新数据到来(且数据量没达到 FULL 阈值)时触发。

  • 场景:处理不定长数据包的神器! 比如接收一帧 JSON 或 AT 指令,你不知道对方发多长,只要对方发完了(总线空闲超时),这个中断就会触发,告诉你“一帧数据接收完毕,可以处理了”。

UART_INTR_TXFIFO_EMPTY(发送 FIFO 空中断)

  • 含义:当发送 FIFO 中的数据量低于设定的阈值时触发(表示缓存快空了)。

  • 场景:用于持续、大量地发送数据。当 FIFO 快空时触发中断,通知 CPU 赶紧往 FIFO 里“喂”新数据,防止发送断流。

UART_INTR_RXFIFO_OVF(接收 FIFO 溢出中断)

  • 含义:接收 FIFO 已经满了,但总线上还在送数据进来,导致新数据被丢弃(溢出) 时触发。

  • 场景:用于错误监控。如果频繁触发此中断,说明你的 CPU 处理数据的速度太慢,或者中断优先级不够,导致数据丢失。


2、错误检测中断

用于检测物理层或协议层的通信异常。

UART_INTR_PARITY_ERR(奇偶校验错误中断)

  • 含义:接收到的数据位计算出的奇偶校验值,与帧中的校验位不匹配时触发。

  • 场景:排查线路干扰或波特率不匹配问题。

UART_INTR_FRAM_ERR(帧错误中断)

  • 含义:在预期的位置没有检测到停止位(本应是高电平,却检测到了低电平)时触发。

  • 场景:通常意味着双方波特率严重不一致,或者总线受到了严重的电磁干扰,导致位同步丢失。

UART_INTR_GLITCH_DET(毛刺检测中断)

  • 含义:在 RX 引脚上检测到了极短的脉冲(毛刺/噪声),其宽度小于设定的过滤阈值时触发。

  • 场景:用于硬件降噪诊断。在工业环境中,如果总线上有高频噪声,可以通过此中断评估信号质量。


3、流控与状态中断

用于硬件或软件流控,防止接收方来不及处理数据。

UART_INTR_CTS_CHG(CTS 信号变化中断)

  • 含义:CTS (Clear To Send) 引脚的电平状态发生跳变(高变低或低变高)时触发。

  • 场景:硬件流控。当对方设备准备好接收数据时,拉低 CTS,触发此中断,通知本方“可以开始发送了”。

UART_INTR_SW_XON(软件流控 XON 中断)

  • 含义:接收到了特定的 XON 字符(通常是0x11/DC1)时触发。

  • 场景:软件流控。对方发来 XON,表示“我缓过来了,请继续发送”。

UART_INTR_SW_XOFF(软件流控 XOFF 中断)

  • 含义:接收到了特定的 XOFF 字符(通常是0x13/DC3)时触发。

  • 场景:软件流控。对方发来 XOFF,表示“我处理不过来了,请暂停发送”。


4、发送控制与 Break 信号中断

涉及发送完成状态以及特殊的 Break(间隔)信号。

UART_INTR_TX_DONE(发送完成中断)

  • 含义:FIFO 里的数据全部发完,且移位寄存器也发完,TX 引脚恢复空闲高电平状态时触发。

  • 场景:RS485 半双工通信必备! 触发此中断意味着数据彻底发出去了,此时可以安全地将 RS485 收发器的控制引脚(DE/RE)拉低,切换为接收模式。

UART_INTR_BRK_DET(Break 间隔检测中断)

  • 含义:在 RX 线上检测到了 Break 信号(持续一段时间的低电平,长于一个数据帧)时触发。

  • 场景:常用于 DMX512 灯光控制协议或 LIN 总线协议,Break 信号通常用作“一帧新数据的同步头”。

UART_INTR_TX_BRK_DONE(发送 Break 完成中断)

  • 含义:硬件发送完 Break 信号(持续低电平阶段结束)时触发。

UART_INTR_TX_BRK_IDLE(发送 Break 后的空闲完成中断)

  • 含义:发送完 Break 信号后,总线恢复空闲状态(高电平)完成时触发。


5、RS485 专属中断

ESP32 的 UART 控制器内置了 RS485 冲突检测等硬件支持。

UART_INTR_RS485_PARITY_ERR(RS485 奇偶校验错误中断)

  • 含义:在 RS485 模式下,接收到的数据奇偶校验错误。

UART_INTR_RS485_FRM_ERR(RS485 帧错误中断)

  • 含义:在 RS485 模式下,接收到的数据帧格式错误。

UART_INTR_RS485_CLASH​​​​​​​(RS485 冲突检测中断)

  • 含义:非常关键! 在半双工 RS485 模式下,芯片一边发送数据,一边把 TX 线上的数据回读。如果回读的数据与发送的数据不一致,说明总线上有其他设备也在同时发送数据,发生了总线冲突(Clash)。

  • 场景:用于多机通信的防碰撞机制,检测到冲突后,主设备可以随机延迟后重发。


6、特殊功能中断

UART_INTR_CMD_CHAR_DET(AT 命令字符检测中断)

  • 含义:硬件在接收流中匹配到了预设的特定字符或字节序列(比如+++或特定的同步头)时触发。

  • 场景:用于快速唤醒或模式切换。比如平时透传数据,一旦检测到特定的 AT 逃逸字符,立刻触发中断让系统进入命令解析模式,无需 CPU 逐字节比对。

UART_INTR_WAKEUP​​​​​​​(唤醒中断)

  • 含义:当芯片处于 Light-sleep(轻度睡眠) 等低功耗模式时,UART 接收到指定数量的数据边沿,将芯片唤醒时触发。

  • 场景:低功耗物联网设备。平时休眠省电,外部设备发数据过来时,通过此中断唤醒 MCU 进行处理。

三、结果展示

http://www.jsqmd.com/news/895520/

相关文章:

  • 电脑显示器哪家好:排名前五专业测评解析 - 服务品牌热点
  • 【C#vsPython·第一阶段】变量声明这件事,C# 和 Python 差了十万八千里
  • GEO优化能不能提高品牌曝光
  • Video Subtitle Remover:3分钟掌握AI视频字幕去除终极技巧
  • AI即架构师:从高成本黑盒到确定性自动化系统的范式转变
  • Web3工程师薪酬变革:代币预算体系的设计与落地实践
  • GEO搜索优化权重规则是什么
  • 2026铸铝门厂家推荐:5家正规铸铝门工厂深度解析,朗鑫领衔铸铝门十大品牌 - 门业测评
  • AMD Ryzen SMU调试工具终极指南:免费解锁硬件底层控制权
  • 智能体系统架构设计:在随机性与确定性间建立清晰边界
  • 猫抓浏览器扩展完整指南:快速解决网页视频下载难题
  • 【CGLIB】`NoOp` 回调的作用是什么?在什么情况下会用到它?
  • 基于MCP协议构建智能求职助手:从架构设计到工程实践
  • ComfyUI移植Ubuntu 26.04:从依赖管理到AI应用部署实战
  • 生产环境部署:Fastify 静态服务 + SPA fallback
  • 终极键盘映射神器:Hitboxer SOCD Cleaner完全使用指南
  • 如何免费解锁Minecraft世界的终极数据编辑神器:NBTExplorer完全指南
  • 归并排序的知识
  • 会议平板哪家好:前五排名 专业深度测评 - 服务品牌热点
  • Embedding 到底是什么:从词向量到句子向量、相似度与局限性
  • 【运维心得】彩色喷墨“只打彩色不打黑”?一招搞定
  • Linux入门篇之启动流程与Vscode远程连接RK3588
  • 2026年4月汽流粉碎机生产厂家哪个好,合金模具/拉伸模具/钛合金模具/粉末冶金模具,汽流粉碎机订做厂家怎么选择 - 品牌推荐师
  • TranslucentTB安装问题解决方案:从错误0x80073D05到完美任务栏透明化
  • 现代作品集重构指南:从展示到论证,打造高价值个人品牌
  • OpenClaw安装后源码精读20260505版本
  • Git2Social:用AI将Git提交自动转化为技术社交媒体内容
  • 2026年知网、维普AIGC检测差距大?论文AI检测该信谁?附4款收藏降重工具 - 降AI实验室
  • 【CGLIB】如何使用 `Dispatcher` 和 `LazyLoader` 实现延迟加载或动态切换代理逻辑?
  • 嵌入式学习之路->stm32篇->(15)通用定时器(下)