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

0 阅读11分钟

目录

前言

一、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 MTMSMTDOGPIO2 GPIO3GPIO4GPIO5
USB1P1_N/P当不使用上述JTAG接口,可用该接口代替JTAG接口的功能USB1P1_N0 USB1P1_P0USB1P1_N1 USB1P1_P1GPIO24GPIO25GPIO26GPIO27
UART0异步通信串口UART0_TXDUART0_RXDGPIO37GPIO38
LP_UART低功耗异步通信串口LP_UART_TXDLP_UART_RXDLP_GPIO14LP_GPIO15
USB专用USB 2.0 接口USB D-USB D+USB_DMUSB_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.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.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,
    };
    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, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
    ESP_ERROR_CHECK(uart_set_mode(uart_port, UART_MODE_UART));

    //中断配置
    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);
    ESP_LOGI(TAG, "UART初始化成功,波特率默认为115200");

    xTaskCreate(uart_intr_callback, "uart_intr", 4096, NULL, 5, NULL);
}
void uart_intr_callback(void *arg)
{
    uart_event_t uart_event;
    int wait_read_size, 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, (size_t *)&wait_read_size);
        len = uart_read_bytes(uart_port, read_data, wait_read_size, 1000);
        read_data[len] = '\0';
        ESP_LOGI(TAG, "uart rx data: %s", read_data);
    }
}

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用来实现硬件自动波特率检测,我这里并未使用。

三、结果展示

​编辑