铝缉惹 发表于 2025-6-11 15:54:15

ESP-IDF教程4 UART 异步通信

1、前提

1.1、基础知识

1.1.1、参数

串口是一种异步通信协议,需要收发双方保持相同的协议才可以正确解析数据,这里不会过多介绍串口协议的相关知识,仅从应用角度来说,一般需要配置如下几个参数:

[*]波特率
[*]数据位
[*]校验位
[*]停止位
[*]流控
在 ESP-IDF 下,每一个参数都都有对应的 API 单独设置或获取该参数,如下所示:

[*]uart_set_baudrate() / uart_get_baudrate()
[*]uart_set_word_length() / uart_get_word_length()
[*]uart_set_parity() / uart_get_parity()
[*]uart_set_stop_bits() / uart_get_stop_bits()
[*]uart_set_hw_flow_ctrl() / uart_set_sw_flow_ctrl() / uart_get_hw_flow_ctrl()
但开发者一般在初始化时不会一个一个单独的设置 UART 的参数,而是使用结构体 uart_config_t 配置参数,最后通过 uart_param_config() 函数执行 UART 的参数配置,如下所示:
void uart_parameter_init(void) {
    const uart_config_t uart_cfg = {
      .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_APB,
    };

    // Initialize UART peripheral with the given configuration
    uart_param_config(UART_PORT_NUM, & uart_cfg);
}1.1.2、输出函数

ESP-IDF 下有多种与串口输出相关的函数,如下所示:

[*]uart_write_bytes
[*]printf
[*]ESP_LOGx
[*]ets_printf
其中 uart_write_bytes 是 #include "driver/uart.h" 驱动库中的函数,其必须在串口显示初始化后才能使用,否则将出现 E (x) uart: uart_write_bytes(1485): uart driver error 的错误。
printf 为标准的 C 函数,ESP_LOGx 系列函数是 ESP-IDF 的日志框架,这两种输出方式通常会使用默认的 UART0 进行输出。由于 ESP32 在上电时 UART0 已经被默认初始化,因此即使不显示初始化 UART0 也可以正常输出。
ESP_LOGx 系列函数默认带回车换行,无需手动添加回车换行,使用时需要包含 #include "esp_log.h" 头文件,主要有如下几种:

[*]ESP_LOGI,信息 Info
[*]ESP_LOGE,错误 Error
[*]ESP_LOGW,警告 Warning
[*]...
ets_printf 是 ESP-ROM 提供的轻量级输出函数,使用时需要包含 #include头文件,该函数不依赖 FreeRTOS,可以直接输出到 UART0,读者可以将其类比于 STM32 HAL 库开发中的 HAL_UART_Transmit 函数,因此在中断中使用也不会卡死,而 printf 则不能在中断中使用。
1.1.3、接收函数

uart_read_bytes() 是 ESP-IDF 中主要的串口接收函数,它的四个参数含义如下:

[*]uart_num,串口号
[*]buf,接收缓存区
[*]length,接收数据长度
[*]ticks_to_wait,阻塞等待时间
上述函数将在以下两种情况下返回:

[*]接收缓存区满
[*]阻塞时间到期
1.1.4、事件

使用 uart_driver_install() 函数安装串口驱动程序时,会涉及到 queue_size 和 uart_queue 两个参数,这两个参数分别设置了队列深度和关联了一个串口队列。
串口驱动安装函数不是用于初始化串口的吗?为什么要创建一个队列?
主要是为了方便管理串口发生的事件,当上述两个参数非空时,串口便可以将驱动层发出的与串口相关的事件自动传递至队列中,用户可以通过 xQueueReceive() 函数来接收事件,方便根据不同的事件分别处理。
串口相关的事件有哪些?
typedef enum {
    UART_DATA,            /* 有数据接收 */
    UART_BREAK,             /* 检测到 Break 信号 */
    UART_BUFFER_FULL,       /* RX 环形缓冲区满 */
    UART_FIFO_OVF,          /* FIFO 溢出 */
    UART_FRAME_ERR,         /* 帧格式错误 */
    UART_PARITY_ERR,      /* 校验错误 */
    UART_DATA_BREAK,
    UART_PATTERN_DET,
#if SOC_UART_SUPPORT_WAKEUP_INT
    UART_WAKEUP,
#endif
    UART_EVENT_MAX,
} uart_event_type_t;当有设备向 ESP32 发送数据时,ESP32 会产生 UART_DATA 事件
ESP32 驱动安装时会设置 TX/RX 缓存区大小,如果设备不断地向 ESP32 发送数据,而 ESP32 没有从其中读出数据,在 RX 缓冲区满的那一刻会产生 UART_BUFFER_FULL 事件
产生 UART_BUFFER_FULL 事件后,如果设备仍然向 ESP32 发送数据,将会不断触发 UART_FIFO_OVF 事件
串口事件使用 uart_event_t 结构体管理,除了包含上述事件类型外还有参数,如下为其具体定义:
typedef struct {
    uart_event_type_t type; /* 串口事件类型 */
    size_t size;            /* UART_DATA 事件的数据大小 */
    bool timeout_flag;      /* UART_DATA 事件数据读超时标志 */
} uart_event_t;串口事件队列一般如何使用?
// 1. 新建串口队列
QueueHandle_t uart_queue;
// 2. 安装驱动时关联串口队列
uart_driver_install(UART_NUM_x, 1024, 0, 10, & uart_queue, 0);

void uart_event_task(void * pvParameters) {
        uart_event_t event;
        while (1) {
                // 3. 接收事件,根据不同的事件做处理
                if (xQueueReceive(uart_queue, (void * ) & event, portMAX_DELAY)) {
                        switch (event.type) {
                                case UART_DATA:
                                        ...
                                        break;
                    case ...:
                              break;
                                ...
            }
      }
    }
}这里为避免混淆,简单说一下串口接收时阻塞、非阻塞、同步和异步的区别,具体如下表所示:
视角类型含义程序视角阻塞函数调用,程序停止,直到返回结果-非阻塞函数调用,立即返回结果用户视角同步函数调用,用户等结果出来,然后做其他事情-异步函数调用,用户不等,有了结果别人通知用户上述两种视角相互结合可以看到如下几种常见组合:

[*]同步阻塞,uart_read_bytes(x, x, portMAX_DELAY)
[*]同步非阻塞,uart_read_bytes(x, x, 0)
[*]异步阻塞,xQueueReceive(x, x, portMAX_DELAY)
[*]异步非阻塞,xQueueReceive(x, x, 0)
可能描述的不那么直观,举个烧水切菜做饭的例子:
模式说明同步阻塞你烧水的时候一直盯着水壶,水开了才去炒菜同步非阻塞你烧水,但一直轮询看有没有水开,同时开始切菜异步阻塞你烧水后坐着等老婆提醒你水开了异步非阻塞你烧水,装个智能水壶,水开了会响铃,你继续安心炒菜1.2、数据结构

ESP-IDF 驱动头文件
#include "driver/uart.h"UART 参数配置结构体
typedef struct {
    int baud_rate;                                                 /* 波特率 */
    uart_word_length_t data_bits;                 /* 数据位 */
    uart_parity_t parity;                                 /* 校验位 */
    uart_stop_bits_t stop_bits;                 /* 停止位 */
    uart_hw_flowcontrol_t flow_ctrl;         /* 硬件流控模式 */
    uint8_t rx_flow_ctrl_thresh;                 /* 硬件 RTS 阈值 */
    union {
      uart_sclk_t source_clk;                 /* 时钟源 */
#if(SOC_UART_LP_NUM >= 1)
      lp_uart_sclk_t lp_source_clk;         /* 低功耗模式下的时钟源 */
#endif
    };
    struct {
      uint32_t allow_pd: 1;                                /* 允许进入低功耗模式*/
      uint32_t backup_before_sleep: 1;         /* 进入低功耗模式前保存状态 */
    }flags;                                                                 /* 配置标志 */
}uart_config_t;1.3、硬件原理图

ESP32 系列芯片在硬件设计时,一般均选择将 UART0 作为默认的输出串口,同时也做控制台输出,本文有关 UART 的硬件原理图如下图所示:

根据 esp32_datasheet_cn.pdf 第 “2.2 管脚概述” 节中 “表 2-1. 管脚概述” 内容可知,U0RXD 默认连接至 GPIO3,U0TXD 默认连接至 GPIO1,如下所示:

其他 ESP32 系列芯片同理,比如 ESP32-C3 ,根据 esp32-c3_datasheet_cn.pdf 第 ”2.3.4 外设管脚分配“ 节中 “表 2-7. 外设管脚分配” 内容可知,U0RXD 默认连接至 GPIO20,U0TXD 默认连接至 GPIO21,如下图所示,因此后续初始化 UART0 时便知道 TX/RX 两个引脚对应的 GPIO 号。

2、示例程序

2.1、UART Transmitter

功能:

[*]通过串口 UART_PORT_NUM 输出数据到串口助手
程序流程:

[*]调用 uart_param_config() 函数初始化串口配置参数
[*]调用 uart_set_pin() 函数绑定串口引脚
[*]调用 uart_driver_install() 函数安装串口驱动,此后即可使用该串口输出/输入数据
[*]调用 uart_write_bytes() / printf() / ESP_LOGI() 等函数通过串口输出数据
示例程序:
#include "freertos/FreeRTOS.h"
#include "driver/uart.h"
#include "string.h"
#include "esp_log.h"

// UART configuration
#define UART_PORT_NUM         UART_NUM_0
#define UART_RX_PIN             3
#define UART_TX_PIN             1
#define UART_RX_BUFFER_SIZE   1024
#define UART_TX_BUFFER_SIZE   1024

// UART parameter configuration
#define UART_BAUD_RATE          115200
#define UART_DATA_BITS          UART_DATA_8_BITS
#define UART_PARITY             UART_PARITY_DISABLE
#define UART_STOP_BITS          UART_STOP_BITS_1
#define UART_FLOW_CTRL          UART_HW_FLOWCTRL_DISABLE

/* The following does not need to be modified */
static const char *TAG = "UART_Transmitter_Example";

void uart_init(void) {
    const uart_config_t uart_cfg = {
      .baud_rate = UART_BAUD_RATE,
      .data_bits = UART_DATA_BITS,
      .parity = UART_PARITY,
      .stop_bits = UART_STOP_BITS,
      .flow_ctrl = UART_FLOW_CTRL,
      .source_clk = UART_SCLK_APB,
    };

    // Initialize UART peripheral with the given configuration
    uart_param_config(UART_PORT_NUM, & uart_cfg);

    // Set UART pins
    uart_set_pin(UART_PORT_NUM, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    // Install UART driver and get the queue handle
    uart_driver_install(UART_PORT_NUM, UART_RX_BUFFER_SIZE, UART_TX_BUFFER_SIZE, 0, NULL, 0);
}

void app_main(void) {

    uart_init();

    while(1) {
      const char *data_1 = "method_1: uart_write_bytes()\r\n";
      uart_write_bytes(UART_PORT_NUM, data_1, strlen(data_1));
      vTaskDelay(500);
      
      printf("method_2: printf()\r\n");
      vTaskDelay(500);

      ESP_LOGI(TAG, "method_3: ESP_LOGx()\r\n");
      vTaskDelay(500);
    }
        // Will not execute
        uart_driver_delete(UART_PORT_NUM);
}2.2、UART Receiver

功能:

[*]通过串口 UART_PORT_NUM 阻塞的接收数据,并将接收到的数据回显
程序流程:

[*]同 "2.1、UART Transmitter" 初始化 UART 流程
[*]调用 uart_read_bytes() 函数阻塞的接收串口数据
[*]如果接收到数据,调用 uart_write_bytes() 函数将数据回显给发送方
示例程序:
#include "freertos/FreeRTOS.h"
#include "driver/uart.h"

// UART configuration
#define UART_PORT_NUM         UART_NUM_0
#define UART_RX_PIN             3
#define UART_TX_PIN             1
#define UART_RX_BUFFER_SIZE   1024
#define UART_TX_BUFFER_SIZE   1024

// UART parameter configuration
#define UART_BAUD_RATE          115200
#define UART_DATA_BITS          UART_DATA_8_BITS
#define UART_PARITY             UART_PARITY_DISABLE
#define UART_STOP_BITS          UART_STOP_BITS_1
#define UART_FLOW_CTRL          UART_HW_FLOWCTRL_DISABLE

/* The following does not need to be modified */
void uart_init(void) {
    const uart_config_t uart_cfg = {
      .baud_rate = UART_BAUD_RATE,
      .data_bits = UART_DATA_BITS,
      .parity = UART_PARITY,
      .stop_bits = UART_STOP_BITS,
      .flow_ctrl = UART_FLOW_CTRL,
      .source_clk = UART_SCLK_APB,
    };
   
    // Initialize UART peripheral with the given configuration
    uart_param_config(UART_PORT_NUM, & uart_cfg);

    // Set UART pins
    uart_set_pin(UART_PORT_NUM, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    // Install UART driver and get the queue handle
    uart_driver_install(UART_PORT_NUM, UART_RX_BUFFER_SIZE, UART_TX_BUFFER_SIZE, 0, NULL, 0);
}


void app_main(void) {

    uart_init();

    uint8_t data;
    while (1) {
      int len = uart_read_bytes(UART_PORT_NUM, data, sizeof(data) - 1, 0);
      if (len > 0) {
            data = '\r';
            data = '\n';
            data = '\0';
            uart_write_bytes(UART_PORT_NUM, (char * ) data, len + 2);
      }
      vTaskDelay(10);
    }
    // Will not execute
        uart_driver_delete(UART_PORT_NUM);
}2.3、UART Receiver Using Event Queue

功能:

[*]通过串口 UART_PORT_NUM 接收数据,并将接收到的数据回显
程序流程:

[*]同 "2.1、UART Transmitter" 初始化 UART 流程,但要额外注意 uart_driver_install() 函数的 uart_queue 参数
[*]调用 xQueueReceive() 函数处理 UART 事件
[*]如果是 UART_DATA 事件,调用 uart_write_bytes() 函数将数据回显给发送方,同时对比接收到的数据,如果为 “on” 则打开 LED 灯,否则为 “off” 则关闭 LED 灯
示例程序:
#include "freertos/FreeRTOS.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "string.h"

#define LED_GPIO_PIN            GPIO_NUM_13

// UART configuration
#define UART_PORT_NUM         UART_NUM_0
#define UART_RX_PIN             3
#define UART_TX_PIN             1
#define UART_RX_BUFFER_SIZE   1024
#define UART_TX_BUFFER_SIZE   1024
#define UART_QUEUE_DEPTH      20

// UART parameter configuration
#define UART_BAUD_RATE          115200
#define UART_DATA_BITS          UART_DATA_8_BITS
#define UART_PARITY             UART_PARITY_DISABLE
#define UART_STOP_BITS          UART_STOP_BITS_1
#define UART_FLOW_CTRL          UART_HW_FLOWCTRL_DISABLE

/* The following does not need to be modified */
static QueueHandle_t uart_queue;

void led_init(void) {

    gpio_config_t io_cfg = {
      .pin_bit_mask = 1 << LED_GPIO_PIN,
      .mode = GPIO_MODE_OUTPUT,
      .pull_up_en = 0,
      .pull_down_en = 0,
      .intr_type = GPIO_INTR_DISABLE,
    };

    gpio_config( & io_cfg);
    // defaut output high level
        gpio_set_level(LED_GPIO_PIN, 1);
}


void uart_init(void) {
    const uart_config_t uart_cfg = {
      .baud_rate = UART_BAUD_RATE,
      .data_bits = UART_DATA_BITS,
      .parity = UART_PARITY,
      .stop_bits = UART_STOP_BITS,
      .flow_ctrl = UART_FLOW_CTRL,
      .source_clk = UART_SCLK_APB,
    };
   
    // Initialize UART peripheral with the given configuration
    uart_param_config(UART_PORT_NUM, & uart_cfg);

    // Set UART pins
    uart_set_pin(UART_PORT_NUM, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    // Install UART driver and get the queue handle
    uart_driver_install(UART_PORT_NUM, UART_RX_BUFFER_SIZE, UART_TX_BUFFER_SIZE, UART_QUEUE_DEPTH, & uart_queue, 0);

}

static void uart_event_task(void * pvParameters) {
    uart_event_t event;
    uint8_t data;

    while (1) {
      if (xQueueReceive(uart_queue, & event, portMAX_DELAY)) {
            switch (event.type) {
                case UART_DATA:
                  int len = uart_read_bytes(UART_PORT_NUM, data, sizeof(data) - 1, 0);
                  if (len > 0) {
                        // add end
                        data = '\r';
                        data = '\n';
                        data = '\0';
                        uart_write_bytes(UART_PORT_NUM, (char * ) data, len + 2);
                        
                        // control logic
                        if(strcmp((const char *)data, "on\r\n") == 0) {
                            gpio_set_level(LED_GPIO_PIN, 1);
                        } else if(strcmp((const char *)data, "off\r\n") == 0) {
                            gpio_set_level(LED_GPIO_PIN, 0);
                        }
                  }
                  break;
                default:
                  printf("Unhandled UART event type: %d\r\n", event.type);
                  break;
            }
      } else {
                        vTaskDelay(10);
      }
    }
}


void app_main(void) {
    led_init();
        uart_init();
    xTaskCreate(uart_event_task, "uart_event_task", 4096, NULL, 12, NULL);
}3、常用函数

// 串口参数配置
esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config);
// 串口引脚配置
esp_err_t uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num);
// 串口驱动注册
esp_err_t uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, int event_queue_size, QueueHandle_t *uart_queue, int intr_alloc_flags);
// 串口阻塞接收数据
int uart_read_bytes(uart_port_t uart_num, void *buf, uint32_t length, TickType_t ticks_to_wait);
// 串口发送数据
int uart_write_bytes(uart_port_t uart_num, const void *src, size_t size);
// 串口驱动注销
esp_err_t uart_driver_delete(uart_port_t uart_num);

// 串口参数配置/获取函数
esp_err_t uart_set_baudrate(uart_port_t uart_num, uint32_t baud_rate);
esp_err_t uart_get_baudrate(uart_port_t uart_num, uint32_t *baudrate);
esp_err_t uart_set_word_length(uart_port_t uart_num, uart_word_length_t data_bit);
esp_err_t uart_get_word_length(uart_port_t uart_num, uart_word_length_t *data_bit);
esp_err_t uart_set_parity(uart_port_t uart_num, uart_parity_t parity_mode);
esp_err_t uart_get_parity(uart_port_t uart_num, uart_parity_t *parity_mode);
esp_err_t uart_set_stop_bits(uart_port_t uart_num, uart_stop_bits_t stop_bit);
esp_err_t uart_get_stop_bits(uart_port_t uart_num, uart_stop_bits_t *stop_bit);4、烧录验证

第 "2.1、UART Transmitter" 节实验现象如下所示,通过三种方式循环从串口发送数据。

第 "2.2、UART Receiver" 节实验现象如下所示,利用串口助手通过串口向 ESP32 发送数据,ESP32 接收到数据后回显给串口助手。

第 "2.3、UART Receiver Using Event Queue" 节实验现象如下所示,通过串口助手发送 “on” 或者 “off” 来控制 LED_GPIO_PIN 的引脚电平状态,并将串口助手发送的数据回显。


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: ESP-IDF教程4 UART 异步通信