尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

ESP32 UART EVENT 串口事件学习(1):从零搭建事件驱动接收框架

发布时间:2026/9/27 22:09:27

资讯中心
01
ARTICLE

ESP32 UART EVENT 串口事件学习(1):从零搭建事件驱动接收框架

ESP32 UART EVENT 串口事件学习(1):从零搭建事件驱动接收框架
1. 为什么轮询串口总丢数据从一次踩坑说起如果你刚开始玩 ESP32 的串口大概率写过这样的代码在while(1)里不停调用uart_read_bytes读到数据就处理读不到就空转。这种轮询方式在波特率低、数据量小的时候看起来没问题但只要数据一密集或者主循环里还有别的耗时任务你就会发现收到的数据缺胳膊少腿。我试过在一个带 WiFi 连接的项目里用轮询读串口结果 WiFi 一握手串口那边就丢了一整包传感器数据排查了半天才意识到是主循环被阻塞导致的。ESP32 的 UART 驱动其实提供了一套更优雅的机制UART EVENT 事件驱动。它的核心思路是串口驱动在底层通过中断感知数据到达、FIFO 溢出、帧错误等状态然后把这些状态打包成事件投递到一个 FreeRTOS 队列里。你的任务只需要阻塞在这个队列上有事件来了就处理没事件就乖乖让出 CPU。这样既不会丢数据也不会浪费 CPU 资源。这篇文章面向刚接触 UART EVENT 的嵌入式开发者我会带你从零搭一个事件驱动的接收框架。你会看到uart_driver_install里那个事件队列参数到底怎么用xQueueReceive怎么和串口事件配合以及怎么用一次完整的触发动作验证事件确实在跑。读完你就能把轮询那套代码换成事件驱动串口接收的稳定性会明显不一样。2. 搭框架前先把 TaoToken 的 Key 和文档准备好在动手写代码之前有个前置动作建议你先做掉把调试过程中需要用到的模型对话和文档入口准备好。嵌入式开发经常遇到寄存器手册看不懂、报错信息查不到的情况这时候有个能随时问的模型入口会省很多时间。你可以先到 TaoToken 的控制台创建一个 API Key地址是 https://taotoken.net/api-keys 这个 Key 后面在配置模型对话或者接入文档查询时会用到。创建完之后建议把接入文档也过一遍位置在 https://taotoken.net/doc 里面会讲清楚请求格式和参数含义避免你后面调接口时反复试错。如果你只是想快速验证某个 UART 事件类型的行为可以直接用模型对话入口 https://taotoken.net/models 问一下比如「UART_PATTERN_DET 事件在什么条件下触发」比翻源码快。要是你打算长期做 ESP32 的编码和 Agent 类项目可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan 它更适合需要持续调用模型的场景。这些入口都带上utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite参数方便你从这篇文章直接跳过去。3. 可复制的 UART 初始化与事件队列配置骨架下面这份代码骨架你可以直接复制到自己的工程里我把它拆成「配置结构体」「驱动安装」「事件任务」三块每块都标了关键参数的含义。3.1 串口参数配置结构体先定义串口的基本通信参数。这里用 UART2因为 UART0 默认被日志占用UART1 的默认引脚又和外接 flash 冲突UART2 的 GPIO16/GPIO17 相对干净。#include driver/uart.h #include freertos/FreeRTOS.h #include freertos/task.h #include freertos/queue.h #include esp_log.h #define EX_UART_NUM UART_NUM_2 #define BUF_SIZE (1024) #define RD_BUF_SIZE (BUF_SIZE) #define PATTERN_CHR_NUM (3) static const char *TAG uart_events; static QueueHandle_t uart_queue; 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_APB, };baud_rate要和你的对端设备一致data_bits、parity、stop_bits这三项也必须匹配否则收到的就是乱码或者帧错误事件。source_clk选 APB 时钟在大多数场景下足够稳定。3.2 uart_driver_install 与事件队列创建这是整个事件驱动框架的核心。uart_driver_install的第四个参数是队列深度第五个参数是队列句柄的指针。你传进去一个QueueHandle_t变量的地址驱动安装成功后会把创建好的队列句柄写回这个变量。void uart_init(void) { // 安装驱动RX缓冲区、TX缓冲区、事件队列深度20、队列句柄、中断标志 ESP_ERROR_CHECK(uart_driver_install(EX_UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 20, uart_queue, 0)); // 配置串口参数 ESP_ERROR_CHECK(uart_param_config(EX_UART_NUM, uart_config)); // 设置引脚TXGPIO17, RXGPIO16, RTS/CTS不用 ESP_ERROR_CHECK(uart_set_pin(EX_UART_NUM, GPIO_NUM_17, GPIO_NUM_16, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); // 启用模式检测检测连续3个字符 ESP_ERROR_CHECK(uart_enable_pattern_det_baud_intr(EX_UART_NUM, , PATTERN_CHR_NUM, 9, 0, 0)); // 重置模式队列长度 ESP_ERROR_CHECK(uart_pattern_queue_reset(EX_UART_NUM, 20)); }这里有个细节要注意uart_driver_install的queue_size设成 20意味着事件队列最多缓存 20 个事件。如果你的数据事件非常密集处理又慢队列会满这时候驱动会丢弃新事件。所以事件处理任务里不要做耗时操作尤其是UART_DATA事件要尽快把数据读走。3.3 事件处理任务骨架任务里阻塞在xQueueReceive上收到事件后根据event.type分支处理。下面这份骨架覆盖了最常见的几种事件类型。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 (;;) { // 阻塞等待事件portMAX_DELAY 表示永久等待 if (xQueueReceive(uart_queue, (void *)event, portMAX_DELAY)) { bzero(dtmp, RD_BUF_SIZE); ESP_LOGI(TAG, uart[%d] event:, EX_UART_NUM); switch (event.type) { case UART_DATA: ESP_LOGI(TAG, [UART DATA]: %d, event.size); uart_read_bytes(EX_UART_NUM, dtmp, event.size, portMAX_DELAY); uart_write_bytes(EX_UART_NUM, (const char *)dtmp, event.size); break; case UART_FIFO_OVF: ESP_LOGI(TAG, hw fifo overflow); uart_flush_input(EX_UART_NUM); xQueueReset(uart_queue); break; case UART_BUFFER_FULL: ESP_LOGI(TAG, ring buffer full); uart_flush_input(EX_UART_NUM); xQueueReset(uart_queue); break; case UART_BREAK: ESP_LOGI(TAG, uart rx break); break; case UART_PARITY_ERR: ESP_LOGI(TAG, uart parity error); break; case UART_FRAME_ERR: ESP_LOGI(TAG, uart frame error); break; case UART_PATTERN_DET: uart_get_buffered_data_len(EX_UART_NUM, buffered_size); int pos uart_pattern_pop_pos(EX_UART_NUM); ESP_LOGI(TAG, [PATTERN DETECTED] pos: %d, buffered: %d, pos, buffered_size); if (pos -1) { 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); }然后在app_main里创建任务void app_main(void) { uart_init(); xTaskCreate(uart_event_task, uart_event_task, 4096, NULL, 12, NULL); }任务栈我给了 4096因为里面用了malloc和日志打印栈太小容易溢出。优先级 12 比默认的 1 高保证事件能被及时处理。4. 验证事件触发一次完整的收发动作代码烧进去之后怎么确认事件真的在跑最简单的办法是短接 GPIO16 和 GPIO17让发送的数据直接回到接收端形成一个回环。用一根杜邦线把 GPIO16 和 GPIO17 连起来然后复位 ESP32。打开串口监视器你会看到类似这样的日志I (312) uart_events: uart[2] event: I (312) uart_events: [UART DATA]: 5这说明有 5 个字节的数据到达触发了UART_DATA事件。因为回环的关系这 5 个字节是你之前通过uart_write_bytes发出去的又被自己收回来了。如果你想主动触发一次发送来验证可以在app_main里加一句uart_write_bytes(EX_UART_NUM, hello, 5);烧录后你会看到[UART DATA]: 5的日志紧接着是回显的内容。这就完成了一次完整的事件触发验证发送 → 硬件回环 → 接收中断 → 事件入队 → 任务取出 → 日志打印。再试一下模式检测。连续发送三个比如uart_write_bytes(EX_UART_NUM, , 3);你会看到[PATTERN DETECTED]的日志pos值表示模式在缓冲区中的位置。这个功能在做 AT 指令解析时特别有用不用自己逐字节扫描。5. 本篇常见错误排查5.1 事件队列收不到任何事件最常见的原因是uart_driver_install的队列参数传错了。如果你传的是NULL而不是uart_queue驱动就不会创建事件队列xQueueReceive会一直阻塞。检查一下你的调用是不是写成了uart_driver_install(EX_UART_NUM, BUF_SIZE*2, BUF_SIZE*2, 20, NULL, 0)。另一个可能是引脚接反了。TX 要接对端的 RXRX 要接对端的 TX回环测试时 GPIO16 接 GPIO17 就行。如果接反数据发不出去也收不回来。5.2 UART_FIFO_OVF 频繁出现这个事件表示硬件 FIFO 溢出了说明数据来得太快你的任务处理太慢。解决办法有两个一是提高事件处理任务的优先级让它更快被调度二是增大uart_driver_install里的 RX 缓冲区大小把BUF_SIZE * 2改成BUF_SIZE * 4甚至更大。如果对端发送速率确实很高还可以考虑启用硬件流控把flow_ctrl设成UART_HW_FLOWCTRL_CTS_RTS并接上 RTS/CTS 引脚。5.3 日志里出现乱码或帧错误UART_FRAME_ERR和UART_PARITY_ERR通常意味着通信参数不匹配。检查双方的波特率、数据位、停止位、校验位是否完全一致。还有一个容易忽略的点如果对端是 3.3V 以外的电平比如 5V 的 Arduino需要加电平转换否则可能收到错误帧。5.4 任务栈溢出导致重启如果你在事件任务里做了大量字符串操作或者递归调用4096 的栈可能不够。表现是系统反复重启日志里有***ERROR*** A stack overflow in task uart_event_task。把xTaskCreate的栈参数改成 8192 试试。另外malloc之后记得在任务退出时free虽然这个任务通常不会退出但养成习惯没坏处。6. 下一步把事件驱动接进你的项目事件驱动框架搭好之后你可以把UART_DATA分支里的回显逻辑换成自己的协议解析。比如收到一帧数据后先判断帧头再提取有效载荷最后通过 WiFi 或者蓝牙转发出去。因为事件任务本身不阻塞主循环你完全可以在另一个任务里跑网络通信两边互不干扰。如果你在解析协议时遇到不确定的字段含义或者想快速验证某个事件类型的行为可以用模型对话入口 https://taotoken.net/models 问一下比翻数据手册快。需要长期做 ESP32 编码和 Agent 项目的话Coding Plan 入口在 https://taotoken.net/coding-plan 接入文档在 https://taotoken.net/doc API Key 在 https://taotoken.net/api-keys 。这些入口都带了utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite从这篇文章点过去就能直接到对应页面。下一篇我会讲怎么在事件驱动的基础上做多串口并发接收以及UART_PATTERN_DET在 AT 指令解析里的实战用法。你可以先把今天这份骨架跑通把回环测试的日志截图存下来后面排查问题时对比着看会很有用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。