一、串口原理
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); }仅本人学习他人视频做出的学习笔记。