一、串口原理

esp32s3三个串口外设

UART0 TX-IO43 RX-IO44

UART1 TX-IO17 TX-IO18

UART2 TX-IO7 TX-IO8

ESP32S3串口独特点

1.ESP32S3芯片内部有个交换矩阵,任意引脚都可以配置成uart0、1

2.引导到main函数之前,IDF框架把串口0已经初始化好

3.已经做好printf的重定向 (可以直接用printf函数打印内容在串口显示)

参数配置

波特率 115200

数据位长度 8

停止位 1

奇偶校验位 0

ESP32-UART基本框图

二、代码编写

1.使用串口1普通收发数据

创建文件

bsp组件 bsp_uart.c bsp_uart.h

task线程 task_uart.c task_uart.h 

主要记住修改CMakeLists文件

uart初始化配置

uart_config_t 结构体初始化

uart_param_config 填充结构体参数

uart_set_pin 设置外设号+引脚

uart_driver_install 注册驱动、分配缓冲区等

收发数据

uart_write_bytes 直接调用即可

uart_read_bytes:

(1)阻塞式单字节接收 
 

uint8_t data;
while (1) {
    uart_read_bytes(UART_NUM_1, &data, 1, portMAX_DELAY);  // 一直等
    // 处理收到的 data
}

(2)带超时的n字节读取(定时轮询)

  int len = uart_read_bytes(UART_NUM_1, buf, sizeof(buf), 10 / portTICK_PERIOD_MS);
        if (len > 0) 
        {
                buf[len] = '\0';
                ESP_LOGI(TAG, "len=%d, UART1 Recv: %s", len, buf);
        }

代码

#include "bsp_uart.h"

/**
 * @brief  UART1 初始化函数
 * @note   配置 UART1 的波特率、数据位、校验位、停止位等参数,并安装驱动
 * @param  无
 * @retval 无
 */
void bsp_uart1_init(void)
{
    // UART1 配置结构体
    uart_config_t uart_config = {
        .baud_rate = 115200,                    // 波特率:115200
        .data_bits = UART_DATA_8_BITS,          // 数据位:8位
        .parity = UART_PARITY_DISABLE,          // 校验位:无校验
        .stop_bits = UART_STOP_BITS_1,          // 停止位:1位
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,  // 流控:关闭硬件流控
        .source_clk = UART_SCLK_APB,            // 时钟源:APB总线时钟
    };
    // 配置 UART 参数
    ESP_ERROR_CHECK(uart_param_config(UART1_PORT, &uart_config));
    // 设置 UART 引脚(TX、RX,RTS和CTS不更改)
    ESP_ERROR_CHECK(uart_set_pin(UART1_PORT, UART1_TX_PIN, UART1_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
    // 安装 UART 驱动,接收缓冲区大小1024,发送缓冲区大小0,不使用事件队列
    ESP_ERROR_CHECK(uart_driver_install(UART1_PORT, 1024, 0, 0, NULL, 0));
}
#include "task_uart.h"

// 日志标签,用于 ESP_LOGI 等日志输出时标识来源为 UART1
static const char *TAG = "UART1";

/**
 * @brief  UART1 任务函数
 * @note   初始化 UART1,循环检测接收缓冲区数据,读取后将数据以十六进制格式回传
 * @param  RXParameters 任务参数(未使用)
 * @retval 无
 */
void task_uart1(void *RXParameters)
{
    uint8_t data[20];       // 数据接收缓冲区,最多存放20字节
    char buff[128];         // 字符串格式化缓冲区
    size_t ringbuf_len = 0; // 接收缓冲区当前数据长度

    // ESP_LOGI(TAG, "UART1 initialized successfully.");
    bsp_uart1_init(); // 初始化 UART1 硬件

    while (1)
    {
        // 获取 UART1 接收缓冲区中的数据长度
        uart_get_buffered_data_len(UART1_PORT, &ringbuf_len);
        if (ringbuf_len > 0)
        {
            ESP_LOGI(TAG, "ringbuf_len: %d", ringbuf_len);

            // 从接收缓冲区读取数据,最多读取 ringbuf_len 字节,超时 10ms
            int len = uart_read_bytes(UART1_PORT, &data, ringbuf_len, 10 / portTICK_PERIOD_MS);

            if (len > 0)
            {
                ESP_LOGI(TAG, "LEN: %d", len);
                // 逐个处理接收到的字节,并回传十六进制格式的数据
                for (size_t i = 0; i < len; i++)
                {
                    sprintf(buff, "Received data: 0x%x\r\n", data[i]);
                    uart_write_bytes(UART1_PORT, buff, strlen(buff));
                }
            }
        }
        // 任务延时 10ms,避免占用过多 CPU
        vTaskDelay(pdMS_TO_TICKS(10));

        /******** 阻塞接收一个字节(已废弃示例代码)******
         * 以下代码为阻塞方式接收单字节的示例,当前未使用:
         * 根据接收到的数据控制 LED 状态,并将数据回传
         *
         * if(data==0x0)
         * {
         *     gpio_set_level(LED_GPIO,0);
         *     printf("Received data: %x\n", data);
         * }
         * else if(data==0x1)
         * {
         *     gpio_set_level(LED_GPIO,1);
         *     printf("Received data: %x\n", data);
         * }
         *
         * sprintf(buff,"Received data: %x\n", data);
         * uart_write_bytes(UART1_PORT, buff, strlen(buff));
         **************************************************/
    }
}

/**
 * @brief  创建 UART1 任务
 * @note   使用 FreeRTOS 的 xTaskCreate 创建 task_uart1 任务
 * @param  无
 * @retval 返回任务创建结果,pdPASS 表示成功
 */
BaseType_t bsp_uart1_task_create(void)
{
    BaseType_t ret = xTaskCreate(task_uart1, "task_uart1", 2048, NULL, 5, NULL);
    return ret;
}

2.协议包

组成

(1)包头 

0xAA 占一个字节长度 (接收代码能识别的数据包的开始)

(2)数据长度

LEN 占一个字节长度(知道协议包里有几个字节的真实数据)

(3)数据 

N Bytes 占N个字节长度 (真正传输的数据)

(4)校验和 

Byte 占一个字节长度;取数据和的最后一个字节 (防止数据传输出错)

解析方法

1.一个字节、一个字节的接收数据

2.利用标志位、状态机控制方法

3.组包校验数据是否符合协议包协议:

(1)找头

(2)知道数据长度

(3)保存有效数据

(4)校验有效数据的和

4.提取有用数据

代码

#include "bsp_uart.h"

/**
 * @brief 初始化UART1接口
 * 
 * 该函数配置并初始化ESP32的UART1接口,设置波特率、数据位、停止位等参数,
 * 并安装UART驱动程序。此函数应在使用UART功能之前调用。
 * 
 * 配置参数:
 * - 波特率:115200 bps
 * - 数据位:8位
 * - 校验位:无
 * - 停止位:1位
 * - 流控:禁用
 * - 时钟源:默认
 * - TX引脚:GPIO17
 * - RX引脚:GPIO18
 */
void bsp_uart1_init(void)
{
    // 配置UART参数结构体
    uart_config_t uart_config = {
        .baud_rate = 115200,                    // 波特率设置为115200 bps
        .data_bits = UART_DATA_8_BITS,          // 数据位:8位
        .parity = UART_PARITY_DISABLE,          // 校验位:禁用(无校验)
        .stop_bits = UART_STOP_BITS_1,          // 停止位:1位
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,  // 流控:禁用硬件流控
        .source_clk = UART_SCLK_DEFAULT,        // 时钟源:使用默认时钟
    };
    
    // 配置UART参数,如果出错会触发错误检查机制
    // UART_PORT在头文件中定义为UART_NUM_1
    ESP_ERROR_CHECK(uart_param_config(UART_PORT, &uart_config)); 
    
    // 设置UART引脚分配
    // TX引脚:GPIO17 (UART_TX_PIN)
    // RX引脚:GPIO18 (UART_RX_PIN)
    // RTS和CTS引脚:不使用 (UART_PIN_NO_CHANGE)
    ESP_ERROR_CHECK(uart_set_pin(UART_PORT, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
    
    // 安装UART驱动程序
    // 参数说明:
    // - UART端口:UART_PORT (UART_NUM_1)
    // - 接收缓冲区大小:256字节
    // - 发送缓冲区大小:0 (使用默认值)
    // - 事件队列大小:0 (不使用事件队列)
    // - 中断分配标志:NULL (使用默认值)
    // - 中断标志:0 (使用默认值)
    ESP_ERROR_CHECK(uart_driver_install(UART_PORT, 256, 0, 0, NULL, 0));
}
#include "task_uart.h"

static const char *TAG = "UART1";  // 日志标签
static proto_state_t state = STATE_WAIT_HEAD;     // 当前协议解析状态
static uint8_t frame_buf[FRAME_MAX_LEN];          // 帧数据缓冲区
static uint16_t data_len = 0;                     // 数据长度
static uint16_t data_cnt = 0;                     // 已接收数据计数

/**
 * @brief UART1任务函数
 * 
 * 此任务负责初始化UART并持续监听串口数据。当接收到数据时,
 * 将数据传递给协议解析函数进行处理。
 * 
 * @param pvParameters 任务参数(未使用)
 */
void uart1_task(void *pvParameters)
{
    uint8_t data;
    bsp_uart1_init();  // 初始化UART1硬件
    ESP_LOGI(TAG, "uart1_task start\n");

    while (1)
    {
        // 从UART端口读取一个字节数据,阻塞等待
        // portMAX_DELAY表示无限期等待直到有数据到达
        int ret = uart_read_bytes(UART_PORT, &data, 1, portMAX_DELAY);
        if (ret > 0) {
            // 如果读取成功,则将数据传入协议解析函数进行处理
            proto_parse_byte(data);
        }
    }
}

/**
 * @brief 创建UART1任务
 * 
 * 使用FreeRTOS的xTaskCreate函数创建一个新的任务来处理UART通信。
 * 
 * @return BaseType_t 任务创建结果,pdPASS表示成功,其他值表示失败
 */
BaseType_t uart1_task_create(void)
{
    // 创建UART1任务:
    // - 任务函数:uart1_task
    // - 任务名称:"uart1_task"
    // - 栈深度:4096字节
    // - 任务参数:NULL(不传递参数)
    // - 任务优先级:10(数字越大优先级越高)
    // - 任务句柄:NULL(不需要保存任务句柄)
    return xTaskCreate(uart1_task, "uart1_task", 4096, NULL, 10, NULL);
}

/**
 * @brief 计算数据校验和
 * 
 * 使用简单的累加求和算法计算数据块的校验和。
 * 
 * @param data 指向要计算校验和的数据的指针
 * @param len 数据长度(字节数)
 * @return uint8_t 校验和结果(只保留低8位)
 */
uint8_t checksum_sum(const uint8_t *data, uint16_t len)
{
    uint16_t sum = 0;
    // 遍历所有数据字节并累加
    for (int i = 0; i < len; i++) {
        sum += data[i];
    }
    // 只保留低8位作为校验和结果
    return (uint8_t)(sum & 0xFF);
}

/**
 * @brief 处理完整帧数据
 * 
 * 解析并执行来自上位机的完整命令帧。
 * 目前支持的命令:
 * - 0x01: 控制LED开关
 * 
 * @param data 指向帧数据的指针(不包含帧头、长度和校验字段)
 * @param len 数据长度
 */
void process_frame(const uint8_t *data, uint16_t len)
{
    // 至少需要包含命令字和参数值两个字节
    if (len < 2) {
        ESP_LOGW(TAG, "帧长度不足");
        return;
    }
    ESP_LOGI(TAG, "收到完整帧:");   

    uint8_t cmd = data[0];   // 提取命令字(第一个字节)
    uint8_t val = data[1];   // 提取参数值(第二个字节)
    char buf[20] = {0};      // 发送响应数据的缓冲区
  
    // 根据命令字处理不同指令
    switch (cmd)
    {
        case 0x01:  // 控制 LED 命令
            // 根据参数值控制LED状态
            if (val == 0x00) {
                // 参数0x00:关闭LED(注意硬件连接可能为低电平有效)
                bsp_gpio_set_level(LED_GPIO, 0);
                ESP_LOGI(TAG, "LED 已打开");                 
            } else if (val == 0x01) 
            {
                // 参数0x01:打开LED(注意硬件连接可能为高电平有效)
                bsp_gpio_set_level(LED_GPIO, 1);
                ESP_LOGI(TAG, "LED 已关闭");            
            } else {
                ESP_LOGW(TAG, "无效的LED控制参数: 0x%02X", val);
            }

            // 向上位机发送确认响应
            uart_write_bytes(UART_PORT, "UART1 Recv Data:", strlen("UART1 Recv Data:"));
            sprintf(buf, "0x%X 0x%X", cmd, val);                
            uart_write_bytes(UART_PORT, buf, strlen(buf));
            uart_write_bytes(UART_PORT,  "\n", 1); 

            // 退出switch语句,结束本次命令处理
            break;
            
        default:
            // 不支持的命令
            ESP_LOGW(TAG, "未知命令: 0x%02X", cmd);
            // 退出switch语句
            break;
    }
}

/**
 * @brief 协议解析函数,每接收一个字节调用一次
 * 
 * 使用有限状态机(FSM)解析自定义串口协议数据帧。
 * 帧格式:帧头(1字节) + 长度(1字节) + 数据(N字节) + 校验和(1字节)
 * 
 * 状态转换过程:
 * 1. STATE_WAIT_HEAD: 等待帧头(0xAA)
 * 2. STATE_WAIT_LEN: 等待数据长度字节
 * 3. STATE_WAIT_DATA: 接收指定长度的数据
 * 4. STATE_WAIT_CHECKSUM: 等待并验证校验和
 * 
 * @param byte 接收到的单个字节数据
 */
void proto_parse_byte(uint8_t byte)
{
    // 根据当前状态处理接收到的字节
    switch (state)
    {
        case STATE_WAIT_HEAD:
            // 等待帧头状态
            // 检查是否为有效的帧头标识
            if (byte == FRAME_HEAD) {
                // 收到正确帧头,进入等待长度字段状态
                state = STATE_WAIT_LEN;
            }
            // 如果不是帧头,则继续保持在此状态,丢弃该字节
            // 退出当前case分支
            break;

        case STATE_WAIT_LEN:
            // 等待长度字段状态
            data_len = byte;           // 保存后续数据的长度
            data_cnt = 0;              // 重置数据接收计数器
            // 检查长度是否超出最大允许值
            if (data_len > FRAME_MAX_LEN) {
                // 长度超限,回到等待帧头状态
                ESP_LOGW(TAG, "数据长度超限: %d", data_len);
                state = STATE_WAIT_HEAD;
            } else {
                // 长度合法,进入等待数据状态
                state = STATE_WAIT_DATA;
            }
            // 退出当前case分支
            break;

        case STATE_WAIT_DATA:
            // 接收数据字段状态
            // 将数据存储到缓冲区中
            frame_buf[data_cnt++] = byte;
            // 检查是否已接收完所有数据
            if (data_cnt >= data_len) {
                // 数据接收完成,进入等待校验和状态
                state = STATE_WAIT_CHECKSUM;
            }
            // 否则继续保持在此状态继续接收数据
            // 退出当前case分支
            break;

        case STATE_WAIT_CHECKSUM:
        {
            // 校验和验证状态(使用代码块避免变量作用域问题)
            uint8_t check_recv = byte;                            // 收到的校验和
            uint8_t check_calc = checksum_sum(frame_buf, data_len); // 根据接收数据计算的校验和

            // 比较收到的校验和与计算得出的校验和
            if (check_calc == check_recv) {
                // 校验通过,处理完整帧数据
                ESP_LOGI(TAG, "校验成功,处理数据帧");
                process_frame(frame_buf, data_len);
            } else {
                // 校验失败,记录错误日志
                ESP_LOGE(TAG, "SUM校验错误: recv=0x%02X calc=0x%02X",
                         check_recv, check_calc);
            }
            
            // 无论校验是否通过,都回到等待帧头状态准备接收下一帧
            state = STATE_WAIT_HEAD;
            // 退出当前case分支
            break;
        }
        
        default:
            // 异常状态处理
            // 如果出现未定义的状态,重置为等待帧头状态
            ESP_LOGE(TAG, "未知状态: %d", state);
            state = STATE_WAIT_HEAD;
            // 退出switch语句
            break;
    }
}

3.串口1事件队列模式

修改初始化代码

(1)事件队列大小

(2)队列句柄


void bsp_uart1_events_init(int uart_events_queue_size, QueueHandle_t *uart_events_queue_handle)
{
    // 配置UART参数结构体
    uart_config_t uart_config = {
        .baud_rate = 115200,                    // 波特率设置为115200 bps
        .data_bits = UART_DATA_8_BITS,          // 数据位:8位
        .parity = UART_PARITY_DISABLE,          // 校验位:禁用(无校验)
        .stop_bits = UART_STOP_BITS_1,          // 停止位:1位
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,  // 流控:禁用硬件流控
        .source_clk = UART_SCLK_DEFAULT,        // 时钟源:使用默认时钟
    };

    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_driver_install(UART_PORT, 1024, 0, uart_events_queue_size, uart_events_queue_handle, 0));
}

原理

1.串口接收到数据之后触发中断

2.IDF在中断里创建事件队列

3.在线程里接收事件,读对应长度的数据

#include "task_uart_events.h"

static const char *TAG = "UART1_EVENTS"; 

QueueHandle_t uart_event_queue;

void uart1_events_task(void *pvParameters)
{
    uart_event_t event;
    uint8_t *data = (uint8_t *)malloc(UART_BUFFER_SIZE);
    bsp_uart1_events_init(UART_QUEUE_SIZE, &uart_event_queue);
    while(1)
    {
        // 等待串口事件
        if (xQueueReceive(uart_event_queue, (void *)&event, (TickType_t)portMAX_DELAY)) {
            switch (event.type) {
                // 接收数据
                case UART_DATA:
                    ESP_LOGI(TAG, "收到数据长度: %d", event.size);
                    // 读取数据
                    int len = uart_read_bytes(UART_PORT, data, event.size, 100 / portTICK_PERIOD_MS);
                    if (len > 0) {
                        ESP_LOGI(TAG, "收到数据: ");
                        for (int i = 0; i < len; i++) {
                            printf(" 0x%X ", data[i]);
                        }
                        printf("\n");
                        // 可以在这里添加数据处理逻辑
                    }
                    break;                
                // 其他事件
                case UART_FIFO_OVF:
                    ESP_LOGI(TAG, "UART FIFO 溢出\n");
                    uart_flush_input(UART_PORT);
                    xQueueReset(uart_event_queue);
                    break;
                case UART_BUFFER_FULL:
                    ESP_LOGI(TAG, "UART 缓冲区满\n");
                    uart_flush_input(UART_PORT);
                    xQueueReset(uart_event_queue);
                    break;
                case UART_BREAK:
                    ESP_LOGI(TAG, "UART 中断\n");
                    break;
                case UART_PARITY_ERR:
                    ESP_LOGI(TAG, "UART 校验错误\n");
                    break;
                case UART_FRAME_ERR:
                    ESP_LOGI(TAG, "UART 帧错误\n");
                    break;
                default:
                    ESP_LOGI(TAG, "未知 UART 事件: %d\n", event.type);
                    break;
            }
        }
    }
    
    free(data);
    vTaskDelete(NULL);
    
}

BaseType_t uart1_events_task_create(void)
{
    return xTaskCreate(uart1_events_task, "uart1_events_task", 4096, NULL, 10, NULL);
}

仅本人学习他人视频做出的学习笔记。

更多推荐