下面是一篇我帮你把“URC 分流是什么意思 + 为什么必须做 + ESP-IDF 可直接用的代码框架”合并成一整篇的文章,你直接复制即可 ✅
ESP32 + 4G 模组:URC 分流是什么意思?为什么必须做?(附完整代码)
在 ESP32(C3/S3 等)上接 4G 模组(如 NT26/ML307 等)时,经常会遇到这些问题:
AT+MIPSEND发送偶发失败明明模块后面又回了
OK,程序却提前判定超时运行一段时间后解析错位,甚至崩溃
UART 波特率高一点(如 921600)就更容易出问题
这些问题里,最典型、最致命的根因之一就是:
URC 没有分流,导致 AT 命令响应被异步上报插队污染,解析状态机错位。
本文讲清楚:URC 分流是什么、为什么必须做、以及一份可直接使用的完整代码。
1)URC 分流是什么意思?
✅ URC(Unsolicited Result Code)是什么?
URC 就是你没有发命令,4G 模组自己主动输出的消息,例如:
网络掉线:
+PDP: DEACTTCP 断开:
+CLOSED: 1有数据到了:
+RECV: ...信号变化:
+CSQ: ...(部分模组会异步上报)SIM 状态变化:
+SIM: READY
这些消息会在任意时刻出现,不受你当前 AT 命令流程控制。
✅ 什么是“URC 分流”?
URC 分流= 在接收串口数据时:
如果某一行是 URC(异步上报)
→直接交给 URC handler(队列/回调)处理如果某一行不是 URC
→ 才认为它是当前 AT 命令的响应内容,加入响应缓冲区
2)如果不做 URC 分流,会发生什么?
最经典的灾难场景:
你发送
AT+MIPSEND=...代码开始等待这条命令返回
OK等待过程中,模组突然插入一条 URC:
+RECV: ...或+CLOSED...你的 parser 把 URC 当成命令响应的一部分,导致:
响应结构污染
结束符没匹配到
超时误判
下一条命令读到上一条的
OK
最终表现为:
偶发发送失败
状态错位
卡死/崩溃
特别是你把波特率开到921600时,误码更容易触发这种错位。
3)正确的工程做法(推荐架构)
要稳定,建议同时做到这三点:
✅ ① UART 只用一个接收任务(高优先级)
只要 UART 接收任务被饿死,就会 RX FIFO overflow,后面必乱。
✅ ② 一次只允许一条 AT 命令在飞(全局串行化)
别让多个任务同时发 AT 命令,否则响应会交叉混在一起。
✅ ③ URC 分流(URC 不进入命令响应缓存)
URC → 单独处理
RSP → 填响应缓存,等 OK/ERROR
4)可直接用的 ESP-IDF “URC 分流完整代码”(C语言)
特点:
UART 接收任务负责 “按行切割(CRLF)”
通过 URC 前缀表判断是否 URC
URC 进入 URC 队列/回调
普通响应进入 resp_buf
遇到 OK/ERROR 释放等待
你只需要按 NT26 实际 URC 内容补充
s_urc_prefixes[]即可。
#include <string.h> #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "freertos/queue.h" #include "esp_log.h" #include "driver/uart.h" #define TAG "AT_URC" // ============== UART 配置 ============== #define AT_UART_NUM UART_NUM_1 #define AT_UART_TX 21 #define AT_UART_RX 20 #define AT_UART_BAUD 460800 #define AT_RX_BUF_SIZE 4096 #define AT_LINE_MAX 512 #define AT_RESP_MAX 2048 // ============== 结束符判断 ============== // AT 命令响应一般以 OK / ERROR / +CME ERROR / +CMS ERROR 结束 static inline bool is_final_result_line(const char *line) { if (!line || !line[0]) return false; if (strcmp(line, "OK") == 0) return true; if (strcmp(line, "ERROR") == 0) return true; if (strncmp(line, "+CME ERROR", 10) == 0) return true; if (strncmp(line, "+CMS ERROR", 10) == 0) return true; return false; } // ============== URC 前缀表(你可按 NT26 实际输出补充) ============== // 命中这些前缀的行,认为是 URC(异步上报),不进入命令响应缓存 static const char *s_urc_prefixes[] = { "+RECV", // 来数据了(示例) "+CLOSED", // TCP/UDP 关闭(示例) "+PDP", // PDP 状态变化(示例) "+SIM", // SIM 状态变化(示例) "+CREG", // 注册状态变化(示例) "+CGREG", "+CEREG", "+NTP", // ... 你根据 NT26 实际 URC 加 }; static bool is_urc_line(const char *line) { if (!line || line[0] == '\0') return false; for (size_t i = 0; i < sizeof(s_urc_prefixes)/sizeof(s_urc_prefixes[0]); i++) { const char *p = s_urc_prefixes[i]; size_t n = strlen(p); if (strncmp(line, p, n) == 0) { return true; } } return false; } // ============== URC 输出(队列/回调) ============== typedef struct { char line[AT_LINE_MAX]; } urc_msg_t; static QueueHandle_t s_urc_queue = NULL; // 你可以把 URC 处理放这里(比如 TCP 收到数据就 push 给网络层) static void urc_dispatch(const char *line) { if (!line) return; ESP_LOGW(TAG, "URC: %s", line); if (s_urc_queue) { urc_msg_t msg = {0}; strlcpy(msg.line, line, sizeof(msg.line)); xQueueSend(s_urc_queue, &msg, 0); } } // ============== 命令响应同步结构 ============== typedef struct { SemaphoreHandle_t lock; // 保证一次只有一个 AT 命令在飞 SemaphoreHandle_t done_sem; // 等待 OK/ERROR 的信号 int last_result_ok; // 1=OK, 0=ERROR char resp_buf[AT_RESP_MAX]; // 收集响应内容(不含 URC) } at_context_t; static at_context_t s_at = {0}; // 把一行追加到 resp_buf(注意溢出保护) static void resp_append_line(const char *line) { if (!line) return; size_t cur = strlen(s_at.resp_buf); size_t left = sizeof(s_at.resp_buf) - cur - 2; // \n + \0 if (left == 0) return; size_t need = strlen(line); if (need > left) need = left; memcpy(s_at.resp_buf + cur, line, need); s_at.resp_buf[cur + need] = '\n'; s_at.resp_buf[cur + need + 1] = '\0'; } // ============== UART 行解析:组包 CRLF ============== // 输入 raw bytes,拼成一行一行,遇到 "\r\n" 或 "\n" 就输出一行 static void at_uart_rx_task(void *arg) { uint8_t *rx = malloc(AT_RX_BUF_SIZE); char line[AT_LINE_MAX]; int line_len = 0; while (1) { int n = uart_read_bytes(AT_UART_NUM, rx, AT_RX_BUF_SIZE, pdMS_TO_TICKS(100)); if (n <= 0) continue; for (int i = 0; i < n; i++) { char c = (char)rx[i]; // 忽略 \r if (c == '\r') continue; // 换行:一行结束 if (c == '\n') { if (line_len == 0) continue; line[line_len] = '\0'; line_len = 0; // ======= URC 分流点(核心) ======= // 1) 先判断是不是 URC if (is_urc_line(line)) { urc_dispatch(line); continue; } // 2) 不是 URC → 属于当前命令响应 resp_append_line(line); // 3) 命令结束行 if (is_final_result_line(line)) { s_at.last_result_ok = (strcmp(line, "OK") == 0); xSemaphoreGive(s_at.done_sem); } // ================================ continue; } // 普通字符:累积到 line buffer if (line_len < (AT_LINE_MAX - 1)) { line[line_len++] = c; } else { // 行太长直接丢弃(防止越界写坏内存) line_len = 0; ESP_LOGE(TAG, "Line too long, drop"); } } } free(rx); vTaskDelete(NULL); } // ============== 发送命令并等待响应(核心API) ============== static bool at_send_cmd_wait(const char *cmd, int timeout_ms, char *out_resp, size_t out_len) { if (!cmd) return false; // 串行化:一次只允许一个命令在飞(很重要) xSemaphoreTake(s_at.lock, portMAX_DELAY); // 清空上一条响应 memset(s_at.resp_buf, 0, sizeof(s_at.resp_buf)); s_at.last_result_ok = 0; // 清掉 done 信号(防止残留) xSemaphoreTake(s_at.done_sem, 0); // 发送命令 ESP_LOGI(TAG, ">>> %s", cmd); uart_write_bytes(AT_UART_NUM, cmd, strlen(cmd)); uart_write_bytes(AT_UART_NUM, "\r\n", 2); // 等待 OK/ERROR bool ok = (xSemaphoreTake(s_at.done_sem, pdMS_TO_TICKS(timeout_ms)) == pdTRUE); if (!ok) { ESP_LOGE(TAG, "AT timeout: %s", cmd); xSemaphoreGive(s_at.lock); return false; } // 返回响应文本 if (out_resp && out_len > 0) { strlcpy(out_resp, s_at.resp_buf, out_len); } bool result_ok = (s_at.last_result_ok == 1); ESP_LOGI(TAG, "<<< %s", result_ok ? "OK" : "ERROR"); xSemaphoreGive(s_at.lock); return result_ok; } // ============== 初始化 ============== void at_client_init(void) { uart_config_t cfg = { .baud_rate = AT_UART_BAUD, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 没有 RTS/CTS 就关 .source_clk = UART_SCLK_DEFAULT, }; uart_driver_install(AT_UART_NUM, AT_RX_BUF_SIZE, 0, 0, NULL, 0); uart_param_config(AT_UART_NUM, &cfg); uart_set_pin(AT_UART_NUM, AT_UART_TX, AT_UART_RX, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); s_at.lock = xSemaphoreCreateMutex(); s_at.done_sem = xSemaphoreCreateBinary(); s_urc_queue = xQueueCreate(10, sizeof(urc_msg_t)); // RX task(建议高一点优先级,避免 FIFO overflow) xTaskCreate(at_uart_rx_task, "at_uart_rx", 4096, NULL, 12, NULL); } // ============== 示例:URC 消费任务(可选) ============== static void urc_consumer_task(void *arg) { urc_msg_t msg; while (1) { if (xQueueReceive(s_urc_queue, &msg, portMAX_DELAY) == pdTRUE) { ESP_LOGW(TAG, "[URC consumer] %s", msg.line); // TODO: 在这里解析 URC,例如收到数据/断线/重连等 } } } // ============== 示例:主流程测试 ============== void app_main(void) { at_client_init(); xTaskCreate(urc_consumer_task, "urc_consumer", 4096, NULL, 6, NULL); char resp[512]; // 测试 AT if (at_send_cmd_wait("AT", 1000, resp, sizeof(resp))) { ESP_LOGI(TAG, "AT resp:\n%s", resp); } // 查询版本 at_send_cmd_wait("AT+CGMR", 2000, resp, sizeof(resp)); ESP_LOGI(TAG, "CGMR:\n%s", resp); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } }5)注意事项(很重要)
✅ ① URC 前缀表不要乱加
有些命令响应也以+开头,比如AT+CSQ返回+CSQ: xx,yy。
如果你把+CSQ放进 URC 表,就会误分流。
建议 URC 表只放“确定异步”的前缀(比如+RECV、+CLOSED、+PDP等)。
✅ ② 必须避免并发 AT
很多人犯错是:多个任务都在发 AT。
正确做法是:
所有 AT 发送统一通过 at_send_cmd_wait()(它内部互斥锁保证串行)
✅ ③ 行长度必须有限制
我代码里做了AT_LINE_MAX限制,超过直接丢弃,防止越界写坏内存。
6)总结一句话
URC 分流就是:URC 不进命令响应缓存,命令响应只等 OK/ERROR。
它能直接解决 4G 模组开发里最烦的:
✅ 响应错位 ✅ 超时误判 ✅ 发送偶发失败 ✅ 乱序崩溃
如果你把 NT26 实际输出的一段日志贴我(包含 URC 那些行),我可以帮你把s_urc_prefixes[]配得更精准,并补上一个“带数据发送”的AT+SEND/MIPSEND安全模板。