【ESP32-IDF笔记】09-UART配置和使用

环境配置

Visual Studio Code :版本1.98.2

ESP32:ESP32-S3

ESP-IDF:V5.4

支持型号:ESP32、ESP32-C2、ESP32-C3、ESP32-C5、ESP32-C6、ESP32-C61、ESP32-H2、ESP32-P4、 ESP32-S2、ESP32-S3

简介

通用异步接收器/发送器 (UART) 属于一种硬件功能,通过使用 RS232、RS422、RS485 等常见异步串行通信接口来处理通信时序要求和数据帧。UART 是实现不同设备之间全双工或半双工数据交换的一种常用且经济的方式。

ESP32-S3 芯片有 3 个 UART 控制器(也称为端口),每个控制器都有一组相同的寄存器以简化编程并提高灵活性。

每个 UART 控制器可以独立配置波特率、数据位长度、位顺序、停止位位数、奇偶校验位等参数。所有具备完整功能的 UART 控制器都能与不同制造商的 UART 设备兼容,并且支持红外数据协会 (IrDA) 定义的标准协议。

主要特性

UART 控制器具有如下特性:

  • 支持三个可预分频的时钟源
  • 可编程收发波特率
  • 三个 UART 的发送 FIFO 以及接收 FIFO 共享 1024 x 8-bit RAM
  • 全双工异步通信
  • 支持输入信号波特率自检功能
  • 支持 5/6/7/8 位数据长度
  • 支持 1/1.5/2 个停止位
  • 支持奇偶校验位
  • 支持 AT_CMD 特殊字符检测
  • 支持 RS485 协议
  • 支持 IrDA 协议
  • 支持 GDMA 高速数据通信
  • 支持 UART 唤醒模式
  • 支持软件流控和硬件流控

UART 架构


图 26.3-2 为 UART 基本架构图。UART 模块工作在两个时钟域:APB_CLK 时钟域和 Core 时钟域。UART Core有三个时钟源:80-MHz APB_CLK、RC_FAST_CLK 以及晶振时钟 XTAL_CLK(详情请参考章节 7 复位和时钟)。可以通过配置 UART_SCLK_SEL 来选择时钟源。分频器用于对时钟源进行分频,然后产生时钟信号来驱动UART Core 模块。

UART 控制器可以分为两个功能块:发送块和接收块。

发送块包含一个发送 FIFO 用于缓存待发送的数据。软件可以通过 APB 总线向 Tx_FIFO 写数据,也可以通过GDMA 将数据搬入 Tx_FIFO。Tx_FIFO_Ctrl 用于控制 Tx_FIFO 的读写过程,当 Tx_FIFO 非空时,Tx_FSM 通过Tx_FIFO_Ctrl 读取数据,并将数据按照配置的帧格式转化成比特流。比特流输出信号 txd_out 可以通过配置UART_TXD_INV 寄存器实现取反功能。

接收块包含一个接收 FIFO 用于缓存待处理的数据。输入比特流 rxd_in 可以输入到 UART 控制器。可以通过UART_RXD_INV 寄存器实现取反。Baudrate_Detect 通过检测最小比特流输入信号的脉宽来测量输入信号的波特率。Start_Detect 用于检测数据的 START 位,当检测到 START 位之后,Rx_FSM 通过 Rx_FIFO_Ctrl 将帧解析后的数据存入 Rx_FIFO 中。软件可以通过 APB 总线读取 Rx_FIFO 中的数据也可以使用 GDMA 方式进行数据接收。

HW_Flow_Ctrl 通过标准 UART RTS 和 CTS(rtsn_out 和 ctsn_in)流控信号来控制 rxd_in 和 txd_out 的数据流。SW_Flow_Ctrl 通过在发送数据流中插入特殊字符以及在接收数据流中检测特殊字符来进行数据流的控制。

当 UART 处于 Light-sleep 状态(详情请参考章节 10 低功耗管理 (RTC_CNTL))时,Wakeup_Ctrl 开始计算rxd_in 的上升沿个数,当上升沿个数大于等于(UART_ACTIVE_THRESHOLD + 3)时产生 wake_up 信号给 RTC模块,由 RTC 来唤醒 ESP32-S3 芯片。

功能描述

UART RAM

ESP32-S3 芯片中三个 UART 控制器共用 1024 x 8-bit RAM 空间。如图 26.4-1 所示,RAM 以 block 为单位进行分配,1 block 为 128x8 bits。图 26.4-1 所示为默认情况下三个 UART 控制器的 Tx_FIFO 和 Rx_FIFO 占用 RAM的情况。通过配置 UART_TX_SIZE 可以对 UARTn 的 Tx_FIFO 以 1 block 为单位进行扩展,通过配置UART_RX_SIZE 可以对 UARTn 的 Rx_FIFO 以 1 block 为单位进行扩展:

  • UART0 Tx_FIFO 可以从地址 0 扩展到整个 RAM 空间;
  • UART1 Tx_FIFO 可以从地址 128 扩展到 RAM 的尾地址;
  • UART2 Tx_FIFO 可以从地址 256 扩展到 RAM 的尾地址;
  • UART0 Rx_FIFO 可以从地址 512 扩展到 RAM 的尾地址;
  • UART1 Rx_FIFO 可以从地址 640 扩展到 RAM 的尾地址;
  • UART2 Rx_FIFO 可以从地址 768 扩展到 RAM 的尾地址。

需要注意的是所有 UART 的 FIFO 起始地址是固定的,因此前一个 UART 的 FIFO 空间向后扩展会占用后面 UART的 FIFO 空间。比如,设置 UART0 的 UART_TX_SIZE 为 2,则 UART0 Tx_FIFO 的地址从 0 扩展到 255。这时,UART1 Tx_FIFO 的默认空间被占用,这时将不能使用 UART1 发送器功能。

当三个 UART 控制器都不工作时,可以通过置位 UART_MEM_FORCE_PD 来使 RAM 进入低功耗状态。

UARTn 的 Tx_FIFO 可以通过置位 UART_TXFIFO_RST 来复位, UARTn 的 Rx_FIFO 可以通过置位UART_RXFIFO_RST 来复位。

配置 UART_TXFIFO_EMPTY_THRHD 可以设置 Tx_FIFO 空信号阈值,当存储在 Tx_FIFO 中的数据量等于或小于UART_TXFIFO_EMPTY_THRHD 时会产生中断 UART_TXFIFO_EMPTY_INT;配置 UART_RXFIFO_FULL_THRHD可以设置 Rx_FIFO 满信号阈值,当储存在 Rx_FIFO 中的数据量大于 UART_RXFIFO_FULL_THRHD 会产生中断UART_RXFIFO_FULL_INT。另外,当 Rx_FIFO 中储存的数据量超过其能存储的最大值时,会产生UART_RXFIFO_OVF_INT 中断。

对于 TX FIFO 和 RX FIFO 的访问,可以通过 APB 总线或者 GDMA 外设接口两种方式。通过 APB 总线访问,即通过寄存器 UART_FIFO_REG 访问 FIFO。您可以写 UART_RXFIFO_RD_BYTE 将数据存入 TX FIFO,也可以读取该字段获取 RX FIFO 中的数据。

唤醒

UART0 和 UART1 支持唤醒功能。当 UART 处于 Light-sleep 状态时,Wakeup_Ctrl 开始统计 rxd_in 的上升沿个数,当上升沿个数大于等于(UART_ACTIVE_THRESHOLD + 3)时产生 wake_up 信号给 RTC 模块,由 RTC 来唤醒 ESP32-S3 芯片。

使用 UART 唤醒之后,需要通过在 Active 模式下向 UART 传输数据或是复位整个 UART 模块清除 wake_up 信号,否则下一次唤醒所需的上升沿个数将减少。

回环功能

UARTn 支持回环功能。置位 UART_LOOPBACK 即开启 UART 的回环测试功能。此时 UART 的输出信号 txd_out和其输入信号 rxd_in 相连,rtsn_out 和 ctsn_in 相连,dtrn_out 和 dsrn_out 相连。数据之后通过 txd_out 发送。当接收的数据与发送的数据相同时表明 UART 能够正常发送和接收数据。

UART 中断

  • UART_AT_CMD_CHAR_DET_INT:当接收器检测到 AT_CMD 字符时触发此中断。
  • UART_RS485_CLASH_INT:在 RS485 模式下检测到发送器和接收器之间的冲突时触发此中断。
  • UART_RS485_FRM_ERR_INT:在 RS485 模式下检测到发送块发送的数据帧错误时触发此中断。
  • UART_RS485_PARITY_ERR_INT:在 RS485 模式下检测到发送块发送的数据校验位错误时触发此中断。
  • UART_TX_DONE_INT:当发送器发送完 FIFO 中的所有数据时触发此中断。
  • UART_TX_BRK_IDLE_DONE_INT:发送器发送完最后一个数据后保持空闲状态时触发此中断。标记为空闲状态的最短时间由阈值决定(可配置)。
  • UART_TX_BRK_DONE_INT:当发送 FIFO 中的数据发送完之后发送器完成了发送 NULL 则触发此中断。
  • UART_GLITCH_DET_INT:当接收器在起始位的中点处检测到毛刺时触发此中断。
  • UART_SW_XOFF_INT:UART_SW_FLOW_CON_EN 置位时,当接收器接收到 XOFF 字符时触发此中断。
  • UART_SW_XON_INT:UART_SW_FLOW_CON_EN 置位时,当接收器接收到 XON 字符时触发此中断。
  • UART_RXFIFO_TOUT_INT:当接收器接收一个字节的时间大于 UART_RX_TOUT_THRHD 时触发此中断。
  • UART_BRK_DET_INT:当接收器在停止位之后检测到一个 NULL(即传输一个 NULL 的时间内保持逻辑低电平)时触发此中断。
  • UART_CTS_CHG_INT:当接收器检测到 CTSn 信号的沿变化时触发此中断。
  • UART_DSR_CHG_INT:当接收器检测到 DSRn 信号的沿变化时触发此中断。
  • UART_RXFIFO_OVF_INT:当接收器接收到的数据量多于 FIFO 的存储量时触发此中断。
  • UART_FRM_ERR_INT:当接收器检测到数据帧错误时触发此中断。
  • UART_PARITY_ERR_INT:当接收器检测到校验位错误时触发此中断。
  • UART_TXFIFO_EMPTY_INT:当发送 FIFO 中的数据量少于 UART_TXFIFO_EMPTY_THRHD 所指定的值时触发此中断。
  • UART_RXFIFO_FULL_INT:当接收器接收到的数据多于 UART_RXFIFO_FULL_THRHD 所指定的值时触发此中断。
  • UART_WAKEUP_INT:UART 被唤醒时产生此中断

功能概述

下文介绍了如何使用 UART 驱动程序的函数和数据类型在 ESP32-S3 和其他 UART 设备之间建立通信。基本编程流程分为以下几个步骤:

  1. 设置通信参数 - 设置波特率、数据位、停止位等
  2. 设置通信管脚 - 分配连接设备的管脚
  3. 安装驱动程序 - 为 UART 驱动程序分配 ESP32-S3 资源
  4. 运行 UART 通信 - 发送/接收数据
  5. 使用中断 - 触发特定通信事件的中断
  6. 删除驱动程序 - 如无需 UART 通信,则释放已分配的资源

步骤 1 到 3 为配置阶段,步骤 4 为 UART 运行阶段,步骤 5 和 6 为可选步骤。

UART 驱动程序函数通过 uart_port_t 识别不同的 UART 控制器。调用以下所有函数均需此标识。

设置通信参数

UART 通信参数可以在一个步骤中完成全部配置,也可以在多个步骤中单独配置。

一次性配置所有参数

调用函数 uart_param_config() 并向其传递 uart_config_t 结构体,uart_config_t 结构体应包含所有必要的参数。请参考以下示例。

复制代码
const uart_port_t uart_num = UART_NUM_2;
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_CTS_RTS,
    .rx_flow_ctrl_thresh = 122,
};
// Configure UART parameters
ESP_ERROR_CHECK(uart_param_config(uart_num, &uart_config));
分步依次配置每个参数

调用下表中的专用函数,能够单独配置特定参数。如需重新配置某个参数,也可使用这些函数。

单独配置特定参数的函数

配置参数 函数
波特率 uart_set_baudrate()
传输位 调用 uart_set_word_length() 设置 uart_word_length_t
奇偶控制 调用 uart_parity_t 设置 uart_set_parity()
停止位 调用 uart_set_stop_bits() 设置 uart_stop_bits_t
硬件流控模式 调用 uart_set_hw_flow_ctrl() 设置 uart_hw_flowcontrol_t
通信模式 调用 uart_set_mode() 设置 uart_mode_t

表中每个函数都可使用 _get_ 对应项来查看当前设置值。例如,查看当前波特率值,请调用 uart_get_baudrate()

设置通信管脚

通信参数设置完成后,可以配置其他 UART 设备连接的 GPIO 管脚。调用函数 uart_set_pin(),指定配置 Tx、Rx、RTS 和 CTS 信号的 GPIO 管脚编号。如要为特定信号保留当前分配的管脚编号,可传递宏 UART_PIN_NO_CHANGE

请为不使用的管脚都指定为宏 UART_PIN_NO_CHANGE

c 复制代码
// Set UART pins(TX: IO4, RX: IO5, RTS: IO18, CTS: IO19)
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_2, 4, 5, 18, 19));

安装驱动程序

通信管脚设置完成后,请调用 uart_driver_install() 安装驱动程序并指定以下参数:

  • UART 控制器编号
  • Tx 环形缓冲区的大小
  • Rx 环形缓冲区的大小
  • 指向事件队列句柄的指针
  • 事件队列大小
  • 分配中断的标志

该函数将为 UART 驱动程序分配所需的内部资源。

c 复制代码
// Setup UART buffered IO with event queue
const int uart_buffer_size = (1024 * 2);
QueueHandle_t uart_queue;
// Install UART driver using an event queue here
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_2, uart_buffer_size, \
                                        uart_buffer_size, 10, &uart_queue, 0));

此步骤完成后,可连接外部 UART 设备检查通信。

运行 UART 通信

串行通信由每个 UART 控制器的有限状态机 (FSM) 控制。

发送数据的过程分为以下步骤:

  1. 将数据写入 Tx FIFO 缓冲区
  2. FSM 序列化数据
  3. FSM 发送数据

接收数据的过程类似,只是步骤相反:

  1. FSM 处理且并行化传入的串行流
  2. FSM 将数据写入 Rx FIFO 缓冲区
  3. 从 Rx FIFO 缓冲区读取数据

因此,应用程序仅会通过 uart_write_bytes()uart_read_bytes() 从特定缓冲区写入或读取数据,其余工作由 FSM 完成。

发送数据

发送数据准备好后,调用函数 uart_write_bytes(),并向其传递数据缓冲区的地址和数据长度。该函数会立即或在有足够可用空间时将数据复制到 Tx 环形缓冲区,随后退出。当 Tx FIFO 缓冲区中有可用空间时,中断服务例程 (ISR) 会在后台将数据从 Tx 环形缓冲区移动到 Tx FIFO 缓冲区。调用函数请参考以下代码。

c 复制代码
// Write data to UART.
char* test_str = "This is a test string.\n";
uart_write_bytes(uart_num, (const char*)test_str, strlen(test_str));

函数 uart_write_bytes_with_break()uart_write_bytes() 类似,但在传输结束时会添加串行中断信号。"串行中断信号"意味着 Tx 线保持低电平的时间长于一个数据帧。

复制代码
// Write data to UART, end with a break signal.
uart_write_bytes_with_break(uart_num, "test break\n",strlen("test break\n"), 100);

能够将数据写入 Tx FIFO 缓冲区的另一函数是 uart_tx_chars()。 与 uart_write_bytes() 不同,此函数在没有可用空间之前不会阻塞。相反,它将写入所有可以立即放入硬件 Tx FIFO 的数据,然后返回写入的字节数。

"配套"函数 uart_wait_tx_done() 用于监听 Tx FIFO 缓冲区的状态,并在缓冲区为空时返回。

复制代码
// Wait for packet to be sent
const uart_port_t uart_num = UART_NUM_2;
ESP_ERROR_CHECK(uart_wait_tx_done(uart_num, 100)); // wait timeout is 100 RTOS ticks (TickType_t)
接收数据

一旦 UART 接收了数据,并将其保存在 Rx FIFO 缓冲区中,就需要使用函数 uart_read_bytes() 检索数据。读取数据之前,调用 uart_get_buffered_data_len() 能够查看 Rx FIFO 缓冲区中可用的字节数。请参考以下示例。

c 复制代码
// Read data from UART.
const uart_port_t uart_num = UART_NUM_2;
uint8_t data[128];
int length = 0;
ESP_ERROR_CHECK(uart_get_buffered_data_len(uart_num, (size_t*)&length));
length = uart_read_bytes(uart_num, data, length, 100);

如果不再需要 Rx FIFO 缓冲区中的数据,可以调用 uart_flush() 清空缓冲区。

软件流控

如果硬件流控处于禁用状态,可使用函数 uart_set_rts()uart_set_dtr() 分别手动设置 RTS 和 DTR 信号电平。

通信方式选择

UART 控制器支持多种通信模式,使用函数 uart_set_mode() 可以选择模式。选择特定模式后,UART 驱动程序将处理已连接 UART 设备的相应行为。例如,使用 RTS 线控制 RS485 驱动芯片,能够实现半双工 RS485 通信。

复制代码
// Setup UART in rs485 half duplex mode
ESP_ERROR_CHECK(uart_set_mode(uart_num, UART_MODE_RS485_HALF_DUPLEX));

使用中断

根据特定的 UART 状态或检测到的错误,可以生成许多不同的中断。ESP32-S3 技术参考手册 > UART 控制器 (UART) > UART 中断 和 UHCI 中断 [PDF] 中提供了可用中断的完整列表。调用 uart_enable_intr_mask()uart_disable_intr_mask() 能够分别启用或禁用特定中断。

UART 驱动提供了一种便利的方法来处理特定的中断,即将中断包装成相应的事件。这些事件定义在 uart_event_type_t 中,FreeRTOS 队列功能可将这些事件报告给用户应用程序。

要接收已发生的事件,请调用 uart_driver_install() 函数并获取返回的事件队列句柄。

UART 驱动可处理的事件包括:

  • FIFO 空间溢出 (UART_FIFO_OVF):当接收到的数据超过 FIFO 的存储能力时,Rx FIFO 会触发中断。

    • (可选)配置 FIFO 阈值:在结构体 uart_intr_config_t 中输入阈值,然后调用 uart_intr_config() 使能配置。这有助于驱动及时处理 RX FIFO 中的数据,避免 FIFO 溢出。
    • 启用中断:调用函数 uart_enable_rx_intr()
    • 禁用中断:调用函数 uart_disable_rx_intr()
    c 复制代码
    const uart_port_t uart_num = UART_NUM_2;
    // Configure a UART interrupt threshold and timeout
    uart_intr_config_t uart_intr = {
        .intr_enable_mask = UART_INTR_RXFIFO_FULL | UART_INTR_RXFIFO_TOUT,
        .rxfifo_full_thresh = 100,
        .rx_timeout_thresh = 10,
    };
    ESP_ERROR_CHECK(uart_intr_config(uart_num, &uart_intr));
    
    // Enable UART RX FIFO full threshold and timeout interrupts
    ESP_ERROR_CHECK(uart_enable_rx_intr(uart_num));
  • 模式检测 (UART_PATTERN_DET):在检测到重复接收/发送同一字符的"模式"时触发中断,例如,模式检测可用于检测命令字符串末尾是否存在特定数量的相同字符("模式")。可以调用以下函数:

    • 配置并启用此中断:调用 uart_enable_pattern_det_baud_intr()
    • 禁用中断:调用 uart_disable_pattern_det_intr()
    c 复制代码
    //Set UART pattern detect function
    uart_enable_pattern_det_baud_intr(EX_UART_NUM, '+', PATTERN_CHR_NUM, 9, 0, 0);
  • 其他事件 :UART 驱动可处理的其他事件包括数据接收 (UART_DATA)、环形缓冲区已满 (UART_BUFFER_FULL)、在停止位后检测到 NULL (UART_BREAK)、奇偶校验错误 (UART_PARITY_ERR)、以及帧错误 (UART_FRAME_ERR)。

括号中的字符串为相应的事件名称。请参考 peripherals/uart/uart_events 中处理 UART 事件的示例。

删除驱动程序

如不再需要与 uart_driver_install() 建立通信,则可调用 uart_driver_delete() 删除驱动程序,释放已分配的资源。

宏指令

API 还定义了一些宏指令。例如,UART_HW_FIFO_LEN 定义了硬件 FIFO 缓冲区的长度,UART_BITRATE_MAX 定义了 UART 控制器支持的最大波特率。

应用示例

示例1-uart_async_rxtxtasks

**说明:**演示了通过同一 UART 接口完成两个独立任务的通信。

其中一个任务定期发送 "Hello world",另一个任务接收并打印 UART 接收到的数据。

c 复制代码
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "driver/uart.h"
#include "string.h"
#include "driver/gpio.h"

static const int RX_BUF_SIZE = 1024;

#define TXD_PIN (GPIO_NUM_4)
#define RXD_PIN (GPIO_NUM_5)

void init(void)
{
    const 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,
    };
    // We won't use a buffer for sending data.
    uart_driver_install(UART_NUM_1, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
    uart_param_config(UART_NUM_1, &uart_config);
    uart_set_pin(UART_NUM_1, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}

int sendData(const char* logName, const char* data)
{
    const int len = strlen(data);
    const int txBytes = uart_write_bytes(UART_NUM_1, data, len);
    ESP_LOGI(logName, "Wrote %d bytes", txBytes);
    return txBytes;
}

static void tx_task(void *arg)
{
    static const char *TX_TASK_TAG = "TX_TASK";
    esp_log_level_set(TX_TASK_TAG, ESP_LOG_INFO);
    while (1) {
        sendData(TX_TASK_TAG, "Hello world");
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
}

static void rx_task(void *arg)
{
    static const char *RX_TASK_TAG = "RX_TASK";
    esp_log_level_set(RX_TASK_TAG, ESP_LOG_INFO);
    uint8_t* data = (uint8_t*) malloc(RX_BUF_SIZE + 1);
    while (1) {
        const int rxBytes = uart_read_bytes(UART_NUM_1, data, RX_BUF_SIZE, 1000 / portTICK_PERIOD_MS);
        if (rxBytes > 0) {
            data[rxBytes] = 0;
            ESP_LOGI(RX_TASK_TAG, "Read %d bytes: '%s'", rxBytes, data);
            ESP_LOG_BUFFER_HEXDUMP(RX_TASK_TAG, data, rxBytes, ESP_LOG_INFO);
        }
    }
    free(data);
}

void app_main(void)
{
    init();
    xTaskCreate(rx_task, "uart_rx_task", 1024 * 2, NULL, configMAX_PRIORITIES - 1, NULL);
    xTaskCreate(tx_task, "uart_tx_task", 1024 * 2, NULL, configMAX_PRIORITIES - 2, NULL);
}

示例2-uart_echo

**说明:**演示了使用 UART 接口回显接收到的所有数据。

引脚连接示意:

c 复制代码
  -----------------------------------------------------------------------------------------
  | Target chip Interface | Kconfig Option     | Default ESP Pin      | External UART Pin |
  | ----------------------|--------------------|----------------------|--------------------
  | Transmit Data (TxD)   | EXAMPLE_UART_TXD   | GPIO4                | RxD               |
  | Receive Data (RxD)    | EXAMPLE_UART_RXD   | GPIO5                | TxD               |
  | Ground                | n/a                | GND                  | GND               |
  -----------------------------------------------------------------------------------------

代码

c 复制代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
#include "esp_log.h"

/**
这是一个示例,它将接收到的任何数据通过配置的UART回传给发送者,并且硬件流控制关闭。它不使用UART驱动程序事件队列。


- 端口:配置的UART

- 接收(Rx)缓冲区:开启

- 发送(Tx)缓冲区:关闭

- 流控制:关闭

- 事件队列:关闭

- 引脚分配:见下方定义(参见Kconfig)
 */

#define ECHO_TEST_TXD (GPIO_NUM_4)
#define ECHO_TEST_RXD (GPIO_NUM_5)

#define ECHO_UART_PORT_NUM      (UART_NUM_1)
#define ECHO_UART_BAUD_RATE     (115200)
#define ECHO_TASK_STACK_SIZE    (1024)

static const char *TAG = "UART TEST";

#define BUF_SIZE (1024)

static void echo_task(void *arg)
{
    /* 配置UART驱动程序的参数,包括通信引脚并安装驱动程序。*/
    uart_config_t uart_config = {
        .baud_rate = ECHO_UART_BAUD_RATE,
        .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,
    };
    int intr_alloc_flags = 0;

#if CONFIG_UART_ISR_IN_IRAM
    intr_alloc_flags = ESP_INTR_FLAG_IRAM;
#endif

    ESP_ERROR_CHECK(uart_driver_install(ECHO_UART_PORT_NUM, BUF_SIZE, 0, 0, NULL, intr_alloc_flags));
    ESP_ERROR_CHECK(uart_param_config(ECHO_UART_PORT_NUM, &uart_config));
    ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, ECHO_TEST_TXD , ECHO_TEST_RXD , UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));

    // 配置一个临时缓冲区来处理传入的数据。
    uint8_t *data = (uint8_t *) malloc(BUF_SIZE);

    while (1) {
        // 从串口读数据
        int len = uart_read_bytes(ECHO_UART_PORT_NUM, data, (BUF_SIZE - 1), 20 / portTICK_PERIOD_MS);
        // 写数据到串口
        uart_write_bytes(ECHO_UART_PORT_NUM, (const char *) data, len);
        if (len) {
            data[len] = '\0';
            ESP_LOGI(TAG, "Recv str: %s", (char *) data);
        }
    }
}

void app_main(void)
{
    xTaskCreate(echo_task, "uart_echo_task", ECHO_TASK_STACK_SIZE, NULL, 10, NULL);
}

示例3 uart_echo_rs485

**说明:**这是一个示例,它会在RS485网络中将接收到的UART端口数据原封不动地返回给发送者。该示例使用ESP-IDF UART软件驱动程序,并在RS485半双工传输模式下工作,需要外部连接总线驱动器。此示例展示的方法可以在用户应用程序中用于在RS485网络中发送/接收数据。

复制代码
         VCC ---------------+                               +--------------- VCC
                            |                               |
                    +-------x-------+               +-------x-------+
         RXD <------| RO            |               |             RO|-----> RXD
                    |              B|---------------|B              |
         TXD ------>| DI  MAX483    |    \  /       |    MAX483   DI|<----- TXD
ESP32 BOARD         |               |   RS-485 side |               |  SERIAL ADAPTER SIDE
         RTS --+--->| DE            |    /  \       |             DE|---+
               |    |              A|---------------|A              |   |
               +----| /RE           |               |            /RE|---+-- RTS
                    +-------x-------+               +-------x-------+
                            |                               |
                           ---                             ---

将USB转RS485适配器连接到计算机,然后将适配器的A/B输出线与连接到ESP32芯片的RS485驱动器的相应A/B输出线相连。

引脚说明:

c 复制代码
  ------------------------------------------------------------------------------------------------------------------------------
  |  UART Interface       | #define            | Default pin for ESP32 | Default pins for others   | External RS485 Driver Pin |
  | ----------------------|--------------------|-----------------------|---------------------------|---------------------------|
  | Transmit Data (TxD)   | CONFIG_MB_UART_TXD | GPIO4                | GPIO9                     | DI                        |
  | Receive Data (RxD)    | CONFIG_MB_UART_RXD | GPIO5                | GPIO8                     | RO                        |
  | Request To Send (RTS) | CONFIG_MB_UART_RTS | GPIO18                | GPIO10                    | ~RE/DE                    |
  | Ground                | n/a                | GND                   | GND                       | GND                       |
  ------------------------------------------------------------------------------------------------------------------------------

代码

c 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "driver/uart.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "sdkconfig.h"

/**
 * This is a example which echos any data it receives on UART back to the sender using RS485 interface in half duplex mode.
*/
#define TAG "RS485_ECHO_APP"

// Note: Some pins on target chip cannot be assigned for UART communication.
// Please refer to documentation for selected board and target to configure pins using Kconfig.
#define ECHO_TEST_TXD           (GPIO_NUM_4)
#define ECHO_TEST_RXD           (GPIO_NUM_5)

// RTS for RS485 Half-Duplex Mode manages DE/~RE
#define ECHO_TEST_RTS           (GPIO_NUM_18)

// CTS is not used in RS485 Half-Duplex Mode
#define ECHO_TEST_CTS           (UART_PIN_NO_CHANGE)

#define BUF_SIZE                (127)
#define BAUD_RATE               (115200)

// Read packet timeout
#define PACKET_READ_TICS        (100 / portTICK_PERIOD_MS)
#define ECHO_TASK_STACK_SIZE    (1024*4)
#define ECHO_TASK_PRIO          (10)
#define ECHO_UART_PORT          (UART_NUM_1)

// UART超时阈值=接收引脚上状态不变的符号数量(约10个时钟周期)
#define ECHO_READ_TOUT          (3) // 3.5T * 8 = 28 ticks, TOUT=3 -> ~24..33 ticks

static void echo_send(const int port, const char* str, uint8_t length)
{
    if (uart_write_bytes(port, str, length) != length) {
        ESP_LOGE(TAG, "Send data critical failure.");
        // 在这里添加代码以处理发送失败的情况。
        
    }
}

// 一个使用UART硬件流控制的回显测试示例。
static void echo_task(void *arg)
{
    const int uart_num = ECHO_UART_PORT;
    uart_config_t uart_config = {
        .baud_rate = BAUD_RATE,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .rx_flow_ctrl_thresh = 122,
        .source_clk = UART_SCLK_DEFAULT,
    };

    // Set UART log level
    esp_log_level_set(TAG, ESP_LOG_INFO);

    ESP_LOGI(TAG, "Start RS485 application test and configure UART.");

    // 安装UART驱动程序(这里不需要事件队列)
	// 在这个示例中,我们甚至没有为发送数据使用缓冲区。
    ESP_ERROR_CHECK(uart_driver_install(uart_num, BUF_SIZE * 2, 0, 0, NULL, 0));

    // 配置UART参数
    ESP_ERROR_CHECK(uart_param_config(uart_num, &uart_config));

    ESP_LOGI(TAG, "UART set pins, mode and install driver.");

    // 根据KConfig设置配置UART引脚。
    ESP_ERROR_CHECK(uart_set_pin(uart_num, ECHO_TEST_TXD, ECHO_TEST_RXD, ECHO_TEST_RTS, ECHO_TEST_CTS));

    // 设置RS485半双工模式
    ESP_ERROR_CHECK(uart_set_mode(uart_num, UART_MODE_RS485_HALF_DUPLEX));

    // 设置UART TOUT功能的读取超时
    ESP_ERROR_CHECK(uart_set_rx_timeout(uart_num, ECHO_READ_TOUT));

    // 为UART分配缓冲区
    uint8_t* data = (uint8_t*) malloc(BUF_SIZE);

    ESP_LOGI(TAG, "UART start receive loop.\r");
    echo_send(uart_num, "Start RS485 UART test.\r\n", 24);

    while (1) {
        //从UART读取数据
        int len = uart_read_bytes(uart_num, data, BUF_SIZE, PACKET_READ_TICS);

        //将数据写回UART
        if (len > 0) {
            echo_send(uart_num, "\r\n", 2);
            char prefix[] = "RS485 Received: [";
            echo_send(uart_num, prefix, (sizeof(prefix) - 1));
            ESP_LOGI(TAG, "Received %u bytes:", len);
            printf("[ ");
            for (int i = 0; i < len; i++) {
                printf("0x%.2X ", (uint8_t)data[i]);
                echo_send(uart_num, (const char*)&data[i], 1);
                // 如果得到回车符,请添加一个换行符(用于测试多字节收据/缓冲区)
                if (data[i] == '\r') {
                    echo_send(uart_num, "\n", 1);
                }
            }
            printf("] \n");
            echo_send(uart_num, "]\r\n", 3);
        } else {
            // Echo a "." to show we are alive while we wait for input
            echo_send(uart_num, ".", 1);
            ESP_ERROR_CHECK(uart_wait_tx_done(uart_num, 10));
        }
    }
    vTaskDelete(NULL);
}

void app_main(void)
{
    //一个没有事件队列的UART读写示例。
    xTaskCreate(echo_task, "uart_echo_task", ECHO_TASK_STACK_SIZE, NULL, ECHO_TASK_PRIO, NULL);
}

示例4 uart_events

说明 演示了如何使用 UART 驱动程序处理特殊的 UART 事件,从 UART0 读取数据,并将数据回显到监视控制台。

代码:

c 复制代码
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/uart.h"
#include "esp_log.h"

static const char *TAG = "uart_events";

/**
端口:UART0

接收(Rx)缓冲区:启用

发送(Tx)缓冲区:禁用

流控:禁用

事件队列:启用

引脚分配:TxD(默认),RxD(默认)
 */

#define EX_UART_NUM UART_NUM_0
#define PATTERN_CHR_NUM    (3)         /*!<设置接收器接收到的连续且相同的字符数量。defines a UART pattern*/

#define BUF_SIZE (1024)
#define RD_BUF_SIZE (BUF_SIZE)

static QueueHandle_t uart0_queue;

static void uart_event_task(void *pvParameters)
{
    uart_event_t event;
    size_t buffered_size;
    uint8_t* dtmp = (uint8_t*) malloc(RD_BUF_SIZE);
    for (;;) {
        // 等待UART事件。
        if (xQueueReceive(uart0_queue, (void *)&event, (TickType_t)portMAX_DELAY)) {
            bzero(dtmp, RD_BUF_SIZE);
            ESP_LOGI(TAG, "uart[%d] event:", EX_UART_NUM);
            switch (event.type) {
            
            /*我们最好尽快处理数据事件,因为数据事件的数量会远远超过其他类型的事件。如果我们花费太多时间在数据事件上,队列可能会满。.*/
            case UART_DATA:		//UART接收数据事件
                ESP_LOGI(TAG, "[UART DATA]: %d", event.size);
                uart_read_bytes(EX_UART_NUM, dtmp, event.size, portMAX_DELAY);
                ESP_LOGI(TAG, "[DATA EVT]:");
                uart_write_bytes(EX_UART_NUM, (const char*) dtmp, event.size);
                break;
            
            case UART_FIFO_OVF:	// 检测到硬件FIFO溢出事件
                ESP_LOGI(TAG, "hw fifo overflow");
                // 如果FIFO溢出,你应该考虑为你的应用程序添加流量控制。
                // 断服务程序已经重置了接收FIFO,作为示例,我们直接在这里清空接收缓冲区以便读取更多数据。
                uart_flush_input(EX_UART_NUM);
                xQueueReset(uart0_queue);
                break;            
            case UART_BUFFER_FULL:	//Event of UART ring buffer full
                ESP_LOGI(TAG, "ring buffer full");
                // 如果缓冲区已满,你应该考虑增加缓冲区大小。例如,我们直接在这里刷新接收缓冲区以读取更多数据。
                uart_flush_input(EX_UART_NUM);
                xQueueReset(uart0_queue);
                break;
           
            case UART_BREAK:	//UART接收器检测到中断事件
                ESP_LOGI(TAG, "uart rx break");
                break;
            
            case UART_PARITY_ERR:	// UART奇偶校验错误事件
                ESP_LOGI(TAG, "uart parity error");
                break;
            
            case UART_FRAME_ERR:	//UART帧错误事件 
                ESP_LOGI(TAG, "uart frame error");
                break;
            
            case UART_PATTERN_DET:	//UART模式检测
                uart_get_buffered_data_len(EX_UART_NUM, &buffered_size);
                int pos = uart_pattern_pop_pos(EX_UART_NUM);
                ESP_LOGI(TAG, "[UART PATTERN DETECTED] pos: %d, buffered size: %d", pos, buffered_size);
                if (pos == -1) {
                    // 以前有一个UART_PATTERN_DET事件,但由于模式位置队列已满,无法记录位置。
                    // 我们应该设置更大的队列大小。例如,我们在这里直接清空接收缓冲区。
                    uart_flush_input(EX_UART_NUM);
                } else {
                    uart_read_bytes(EX_UART_NUM, dtmp, pos, 100 / portTICK_PERIOD_MS);
                    uint8_t pat[PATTERN_CHR_NUM + 1];
                    memset(pat, 0, sizeof(pat));
                    uart_read_bytes(EX_UART_NUM, pat, PATTERN_CHR_NUM, 100 / portTICK_PERIOD_MS);
                    ESP_LOGI(TAG, "read data: %s", dtmp);
                    ESP_LOGI(TAG, "read pat : %s", pat);
                }
                break;
            
            default:
                ESP_LOGI(TAG, "uart event type: %d", event.type);
                break;
            }
        }
    }
    free(dtmp);
    dtmp = NULL;
    vTaskDelete(NULL);
}

void app_main(void)
{
    esp_log_level_set(TAG, ESP_LOG_INFO);

    /* 配置UART驱动程序的参数,包括通信引脚并安装驱动程序。 */
    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,
    };
    // 安装UART驱动程序,并获取队列。
    uart_driver_install(EX_UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 20, &uart0_queue, 0);
    uart_param_config(EX_UART_NUM, &uart_config);

    //设置UART日志级别
    esp_log_level_set(TAG, ESP_LOG_INFO);
    //设置UART引脚(使用UART0默认引脚,即不作任何更改。)
    uart_set_pin(EX_UART_NUM, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    //设置UART模式检测功能
    uart_enable_pattern_det_baud_intr(EX_UART_NUM, '+', PATTERN_CHR_NUM, 9, 0, 0);
    //将模式队列长度重置为最多记录20个模式位置。.
    uart_pattern_queue_reset(EX_UART_NUM, 20);

    //创建一个任务来处理来自中断服务程序的UART事件
    xTaskCreate(uart_event_task, "uart_event_task", 3072, NULL, 12, NULL);
}

示例5 uart repl 控制台

说明: 这个示例演示了如何在默认UART之外的另一个UART上使用REPL控制台。它还展示了如何将这两个UART连接在一起,无论是用于测试还是发送命令,无需任何人工交互。

该示例可以在任何至少有两个UART的ESP板上运行。开发板应通过一根USB线连接到PC,用于烧录和监控。如果您愿意监控控制台UART,您可以使用与GPIO引脚兼容的3.3V USB转串口适配器。.

无需外部连接即可运行示例。然而,如前所述,如果您愿意查看第二个UART(控制台UART)上的情况,可以将CONSOLE_UART_TX_PIN(默认为5)和CONSOLE_UART_RX_PIN(默认为4)连接到串口转USB适配器。

示例将默认UART设置为使用DEFAULT_UART_RX_PIN和DEFAULT_UART_TX_PIN。然后,它将在第二个UART上设置REPL控制台。最后,它将连接两个UART,以便默认UART能够向控制台UART发送命令并接收回复。

UART的示意图:

c 复制代码
                  UART default      UART console

USB monitoring <------ TX -----------> RX----+
                                             v
                                       解析命令并输出结果
                                             |                 Optional 3.3V
                       RX <----------- TX<---+  (----------->) Serial-to-USB
                                                                  Adapter

如果一切顺利,串行端口的默认输出应该是"结果:成功"。否则,应该是"结果:失败".

c 复制代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "soc/uart_periph.h"
#include "esp_rom_gpio.h"
#include "driver/gpio.h"
#include "hal/gpio_hal.h"
#include "sdkconfig.h"
#include "esp_console.h"
#include "linenoise/linenoise.h"
#include <string.h>

#define DEFAULT_UART_CHANNEL    (0)
#define CONSOLE_UART_CHANNEL    (1 - DEFAULT_UART_CHANNEL)
#define DEFAULT_UART_RX_PIN     (3)
#define DEFAULT_UART_TX_PIN     (2)
#define CONSOLE_UART_RX_PIN     (4)
#define CONSOLE_UART_TX_PIN     (5)

#define UARTS_BAUD_RATE         (115200)
#define TASK_STACK_SIZE         (2048)
#define READ_BUF_SIZE           (1024)

/* 由"consoletest"命令打印的消息。它也将被默认的UART用于检查第二个UART的回复。由于此处行结束符不标准(\,\\r\,\\r...),我们不在这个字符串中包含它们。 */
const char test_message[] = "This is an example string, if you can read this, the example is a success!";

/**
此函数将默认UART发送端连接到控制台UART接收端,将默认UART接收端连接到控制台UART发送端。目的是通过读取接收FIFO直接向控制台发送命令并获取回复。
 */
static void connect_uarts(void)
{
    esp_rom_gpio_connect_out_signal(DEFAULT_UART_RX_PIN, UART_PERIPH_SIGNAL(1, SOC_UART_TX_PIN_IDX), false, false);
    esp_rom_gpio_connect_in_signal(DEFAULT_UART_RX_PIN, UART_PERIPH_SIGNAL(0, SOC_UART_RX_PIN_IDX), false);

    esp_rom_gpio_connect_out_signal(DEFAULT_UART_TX_PIN, UART_PERIPH_SIGNAL(0, SOC_UART_TX_PIN_IDX), false, false);
    esp_rom_gpio_connect_in_signal(DEFAULT_UART_TX_PIN, UART_PERIPH_SIGNAL(1, SOC_UART_RX_PIN_IDX), false);
}

/**
断开默认的UART与控制台UART的连接,这在测试完成后使用一次,这样我们可以在UART上打印消息而不将其发送回控制台UART。否则,我们将陷入无限循环。
 */
static void disconnect_uarts(void)
{
    esp_rom_gpio_connect_out_signal(CONSOLE_UART_TX_PIN, UART_PERIPH_SIGNAL(1, SOC_UART_TX_PIN_IDX), false, false);
    esp_rom_gpio_connect_in_signal(CONSOLE_UART_RX_PIN, UART_PERIPH_SIGNAL(1, SOC_UART_RX_PIN_IDX), false);
}

/**
配置并安装默认的UART,然后将其连接到控制台UART。
 */
static void configure_uarts(void)
{
    /* Configure parameters of an UART driver,
     * communication pins and install the driver */
    uart_config_t uart_config = {
        .baud_rate = UARTS_BAUD_RATE,
        .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(DEFAULT_UART_CHANNEL, READ_BUF_SIZE * 2, 0, 0, NULL, 0));
    ESP_ERROR_CHECK(uart_param_config(DEFAULT_UART_CHANNEL, &uart_config));
    ESP_ERROR_CHECK(uart_set_pin(DEFAULT_UART_CHANNEL, DEFAULT_UART_TX_PIN, DEFAULT_UART_RX_PIN,
                                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));

    connect_uarts();
}

/**
当调用命令`consoletest`时将被调用的函数。它将简单地打印上面定义的`test_message`。
 */
static int console_test(int argc, char **argv)
{
    printf("%s\n", test_message);
    return 0;
}

/**
在另一个主要任务中执行的功能(如主要任务执行REPL控制台)。
它将发送"consoletest"命令到控制台UART,然后通过RX读取响应。
响应应包含test_message字符串。
 */
static void send_commands(void* arg)
{
    static char data[READ_BUF_SIZE];
    char command[] = "consoletest\n";
    int len = 0;
    void* substring = NULL;

    /*忽略控制台发送的第一条条消息。 */
    do {
        len = uart_read_bytes(DEFAULT_UART_CHANNEL, data, READ_BUF_SIZE, 100 / portTICK_PERIOD_MS);
    } while (len == 0);

    if (len == -1) {
        goto end;
    }
    /* 向控制台UART发送命令`consoletest`. */
    len = uart_write_bytes(DEFAULT_UART_CHANNEL, command, sizeof(command));
    if (len == -1) {
        goto end;
    }

    /* 从控制台获取答案,并给它一些延迟. */
    do {
        len = uart_read_bytes(DEFAULT_UART_CHANNEL, data, READ_BUF_SIZE - 1, 250 / portTICK_PERIOD_MS);
    } while (len == 0);

    if (len == -1) {
        goto end;
    }

    /**
    检查是否可以在接收到的消息中找到test_message。在此之前,我们需要添加一个NULL字符来终止字符串。
     */
    data[len] = 0;
    substring = strcasestr(data, test_message);

end:
    /* 这绝对不能再把任何东西发送到控制台了! */
    disconnect_uarts();
    printf("Result: %s\n", substring == NULL ? "Failure" : "Success");
    vTaskDelete(NULL);
}

void app_main(void)
{
    esp_console_repl_t *repl = NULL;
    esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
    repl_config.prompt = "repl >";
    const esp_console_cmd_t cmd = {
        .command = "consoletest",
        .help = "Test console by sending a message",
        .func = &console_test,
    };
    esp_console_dev_uart_config_t uart_config = {
        .channel = CONSOLE_UART_CHANNEL,
        .baud_rate = UARTS_BAUD_RATE,
        .tx_gpio_num = CONSOLE_UART_TX_PIN,
        .rx_gpio_num = CONSOLE_UART_RX_PIN,
    };
    /**
     * 由于我们没有实际的串行终端,(我们只是使用默认的UART发送和接收命令),
     * 所以我们不会处理任何转义序列,因此最简单的方法是将控制台设置为"哑"模式。 */
    linenoiseSetDumbMode(1);

    ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl));
    configure_uarts();
    
	const esp_console_cmd_t cmd = {
        .command = "consoletest",
        .help = "Test console by sending a message",
        .func = &console_test,
    };
    ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));	// 当然,调用这个函数,可以注册多条指令,很多很多条!再想想这能做什么?!!

    /* 创建一个任务,用于通过第二UART发送和接收命令. */
    xTaskCreate(send_commands, "send_commands_task", TASK_STACK_SIZE, NULL, 10, NULL);

    ESP_ERROR_CHECK(esp_console_start_repl(repl));
}
相关推荐
hdsoft_huge10 分钟前
ESXI虚拟交换机 + H3C S5120交换机 + GR5200路由器组网笔记
笔记·智能路由器
YGY Webgis糕手之路11 分钟前
Cesium 快速入门(八)Primitive(图元)系统深度解析
前端·经验分享·笔记·vue·web
qq_2663487323 分钟前
idea 源码阅读笔记
java·笔记·intellij-idea
hack:D_K1 小时前
网络基础——路由控制
网络·经验分享·笔记·路由
YGY Webgis糕手之路1 小时前
Cesium 快速入门(四)相机控制完全指南
前端·经验分享·笔记·vue·web
我要学脑机1 小时前
freesurfer处理图谱和被试的脑模版对齐的操作
笔记·开源软件
YGY Webgis糕手之路1 小时前
Cesium 快速入门(六)实体类型介绍
前端·经验分享·笔记·vue·web
YGY Webgis糕手之路1 小时前
Cesium 快速入门(一)快速搭建项目
前端·经验分享·笔记·vue·web
就改了2 小时前
Nginx 配置负载均衡(详细版)
笔记
明长歌3 小时前
【javascript】new.target 学习笔记
javascript·笔记·学习