ATParser:嵌入式C语言轻量级AT命令解析库
1. ATParser 库概述
ATParser 是一个轻量级、可移植的嵌入式 C 语言库,专为解析 AT(Attention)命令及结构相似的文本协议而设计。其核心目标并非实现完整的 Modem 控制栈,而是提供一套健壮、无依赖、内存可控的底层解析引擎,使开发者能够快速构建符合 3GPP TS 27.007、ETSI TS 100 916 等标准的 AT 命令处理器,或适配自定义串行协议(如传感器配置指令、工业 PLC 指令集、蓝牙 HCI 文本模式等)。
该库完全遵循 ANSI C89 标准,不依赖任何操作系统抽象层(如 POSIX)、C 标准库中的malloc/printf/string.h等非必需函数,亦不引入 RTOS 或 HAL 层依赖。所有内存操作均通过用户传入的静态缓冲区完成,适用于资源受限的 MCU 平台(如 STM32F0/F1/L0、nRF52、ESP32-C3、RISC-V 架构的 GD32E230 等),典型 RAM 占用低于 256 字节(不含用户缓冲区),Flash 占用约 1.8–2.4 KB(GCC -Os 编译)。
ATParser 的设计哲学是“协议无关、状态显式、错误可溯”。它不预设命令语法树,也不内置命令分发逻辑;相反,它将原始输入流(通常来自 UART 接收中断或 DMA 缓冲区)逐字符解析,识别出命令行(Command Line)、响应行(Response Line)、中间响应(Intermediate Response)、最终结果码(Final Result Code)以及可能的错误信息(Error Indication)。整个过程由有限状态机(FSM)驱动,所有状态转换均对外可见,便于调试与定制。
在实际嵌入式项目中,ATParser 常作为 AT 命令处理中间件,位于硬件驱动层(UART 接收 ISR / DMA callback)与应用逻辑层(AT 命令分发器、状态机控制器、网络连接管理器)之间。例如,在基于 FreeRTOS 的 NB-IoT 终端固件中,UART ISR 将接收到的字节写入环形缓冲区,一个低优先级任务周期性调用ATP_parse(),将新数据喂给解析器;当检测到完整命令行时,触发on_command_received()回调,由上层解析AT+CGATT?并查询附着状态;当收到+CGATT: 1中间响应时,更新本地网络状态变量;最终收到OK后,通过队列通知主任务完成。
2. 协议结构与解析模型
2.1 AT 命令协议基本构成
AT 命令协议虽无单一强制标准,但绝大多数实现(包括 Quectel、SIMCom、u-blox、Telit 模组)均遵循以下通用结构:
[<CR><LF>]AT<command>[=<parameters>][?<CR><LF>]其中:
<CR>(0x0D)和<LF>(0x0A)为行定界符,部分模组支持仅<CR>或仅<LF>;AT为固定前缀,大小写敏感(标准要求大写,但部分模组兼容小写);<command>为命令名,如+CGMI(查询制造商信息)、+CIPSTART(TCP 连接);[=<parameters>]为可选参数字段,用于设置值(如AT+CMGF=1);[?]为可选查询符号,用于读取当前值(如AT+CMGF?);- 行尾必须有
<CR><LF>(部分模组允许<LF><CR>或单<CR>)。
响应则分为三类:
- 最终结果码(Final Result Code):
OK、ERROR、NO CARRIER、CONNECT、RING等,标志命令执行终结; - 中间响应(Intermediate Response):以
+开头的行,如+CME ERROR: 10(手机错误)、+IPD,12:"Hello"(TCP 数据),在最终结果码前出现; - 提示响应(Prompt Response):
>符号,表示模组已准备好接收数据(如透传模式下)。
ATParser 将输入字节流建模为一个线性状态机,其核心状态包括:
| 状态码 | 名称 | 触发条件 | 典型输出事件 |
|---|---|---|---|
ATP_STATE_IDLE | 空闲 | 初始状态,或刚完成一次完整响应解析 | — |
ATP_STATE_CR | 等待 CR | 接收到<CR> | 可能进入ATP_STATE_LF或直接触发行结束 |
ATP_STATE_LF | 等待 LF | 接收到<LF>(且前一字符为<CR>) | 触发ATP_EVENT_LINE_END |
ATP_STATE_AT_PREFIX | 匹配 AT 前缀 | 接收到'A'后紧跟'T' | 设置is_at_command = true |
ATP_STATE_COMMAND | 解析命令名 | 在AT后接收字母/数字/+/-/_ | 累积至cmd_name缓冲区 |
ATP_STATE_EQUALS | 遇到= | 接收到'=' | 设置has_equals = true |
ATP_STATE_PARAMETERS | 解析参数 | 在=后接收任意非<CR>/<LF>字符 | 累积至params缓冲区 |
ATP_STATE_QUERY | 遇到? | 接收到'?'(且不在引号内) | 设置is_query = true |
ATP_STATE_RESULT_CODE | 识别结果码 | 接收到OK、ERROR等关键字 | 触发ATP_EVENT_FINAL_RESULT |
ATP_STATE_INTERMEDIATE | 识别中间响应 | 接收到+开头且非AT行 | 触发ATP_EVENT_INTERMEDIATE |
该状态机严格区分“命令输入”与“响应输出”,不假设双向通信时序,允许异步混杂输入(如命令未发完即收到模组主动上报的+UUSOCL)。
2.2 关键设计决策解析
零拷贝输入设计:
ATP_parse()接口接受(const uint8_t *data, size_t len),内部仅维护指针偏移与状态,不复制原始数据。这对 DMA 接收场景至关重要——可直接将 DMA 完成中断中获取的rx_buffer地址传入,避免额外内存搬运开销。缓冲区所有权分离:命令名、参数、中间响应内容等字符串均不分配内存,而是由用户在
ATP_Context结构体中提供固定长度缓冲区(如char cmd_name[32])。库仅写入并保证\0终止。此举彻底消除动态内存风险,符合 IEC 61508 SIL3 等功能安全要求。引号与转义处理:支持双引号包围的参数(如
AT+CIMI="12345"),内部自动跳过引号内<CR>/<LF>,并识别\"转义序列。此逻辑在atp_parse_char()内部通过in_quotes标志位与escape_next标志位协同实现,确保AT+HTTPDATA=100,5000与AT+HTTPDATA="100,5000",5000被正确区分。超长行截断保护:当某一行字符数超过用户配置的
max_line_length(默认 256)时,状态机强制回退到ATP_STATE_IDLE并触发ATP_EVENT_LINE_OVERFLOW事件。这防止因模组异常输出(如固件崩溃打印大量日志)导致缓冲区溢出。
3. API 接口详解
3.1 核心数据结构
typedef struct { // 用户提供的缓冲区(必须在生命周期内有效) char cmd_name[ATP_CMD_NAME_MAX_LEN]; // 命令名,如 "+CGMI" char params[ATP_PARAMS_MAX_LEN]; // 参数内容,如 "Quectel" char intermediate[ATP_INTERMEDIATE_MAX_LEN]; // 中间响应全文,如 "+CME ERROR: 10" // 解析上下文状态 uint8_t state; // 当前 FSM 状态(ATP_STATE_*) uint8_t in_quotes; // 是否处于双引号内 uint8_t escape_next; // 下一字符是否为转义符 uint8_t is_at_command; // 当前行是否为 AT 命令(而非响应) uint8_t is_query; // 是否为查询命令(含 '?') uint8_t has_equals; // 是否含 '=' uint16_t cmd_len; // cmd_name 实际长度 uint16_t params_len; // params 实际长度 uint16_t intermediate_len; // intermediate 实际长度 // 用户可配置参数 uint16_t max_line_length; // 单行最大允许长度(防溢出) uint16_t line_pos; // 当前行已解析字符数 // 回调函数指针(全为可选,设为 NULL 则忽略) void (*on_command_received)(const struct ATP_Context* ctx); void (*on_intermediate_response)(const struct ATP_Context* ctx); void (*on_final_result)(const struct ATP_Context* ctx, ATP_ResultCode code); void (*on_line_overflow)(const struct ATP_Context* ctx); void (*on_parsing_error)(const struct ATP_Context* ctx, ATP_ParseError err); } ATP_Context;关键字段说明:
max_line_length:建议设为 256(覆盖绝大多数 AT 命令),若需支持AT+HTTPDATA大数据,则需增大,但需同步增加intermediate缓冲区。on_*回调:全部为弱符号(weak symbol)或运行时注册,允许用户选择性实现。例如,仅关心最终结果时,只实现on_final_result;若需调试,可实现on_parsing_error打印err枚举值。
3.2 主要函数接口
void ATP_init(ATP_Context* ctx, uint16_t max_line_len)
初始化上下文,重置所有状态字段。必须在首次调用ATP_parse()前执行。
ATP_Context atp_ctx; ATP_init(&atp_ctx, 256); atp_ctx.on_final_result = my_on_final_result; atp_ctx.on_intermediate_response = my_on_intermediate;ATP_Event ATP_parse(ATP_Context* ctx, const uint8_t* data, size_t len)
核心解析函数。逐字节处理输入,返回事件类型,指示上层应如何响应。
| 返回值 | 含义 | 典型处理方式 |
|---|---|---|
ATP_EVENT_NONE | 无事件,继续喂入数据 | 忽略,等待更多数据 |
ATP_EVENT_LINE_END | 一行结束(遇到<CR><LF>) | 检查is_at_command判断是否为命令行 |
ATP_EVENT_FINAL_RESULT | 识别到最终结果码 | 调用on_final_result,重置命令状态 |
ATP_EVENT_INTERMEDIATE | 识别到中间响应 | 调用on_intermediate_response |
ATP_EVENT_LINE_OVERFLOW | 行超长 | 调用on_line_overflow,丢弃当前行 |
ATP_EVENT_PARSING_ERROR | 解析错误(如非法字符) | 调用on_parsing_error |
典型调用循环(FreeRTOS 任务中):
void at_parser_task(void *pvParameters) { UART_HandleTypeDef *huart = (UART_HandleTypeDef*)pvParameters; uint8_t rx_buf[64]; ATP_Context atp_ctx; ATP_init(&atp_ctx, 256); for(;;) { HAL_StatusTypeDef ret = HAL_UART_Receive(huart, rx_buf, sizeof(rx_buf), 100); if (ret == HAL_OK && atp_ctx.line_pos < sizeof(rx_buf)) { ATP_Event evt = ATP_parse(&atp_ctx, rx_buf, atp_ctx.line_pos); switch(evt) { case ATP_EVENT_FINAL_RESULT: if (atp_ctx.result_code == ATP_RESULT_OK) { // 命令执行成功,可发送下一条 send_next_at_command(); } break; case ATP_EVENT_INTERMEDIATE: handle_intermediate(&atp_ctx); // 如解析 +CIPSTATUS break; case ATP_EVENT_LINE_OVERFLOW: // 清空缓冲区,重置解析器 ATP_init(&atp_ctx, 256); break; } } vTaskDelay(1); } }const char* ATP_result_code_to_str(ATP_ResultCode code)
辅助函数,将枚举值转为字符串,便于日志输出。
// 在 on_final_result 回调中: void my_on_final_result(const ATP_Context* ctx, ATP_ResultCode code) { printf("Final result: %s\n", ATP_result_code_to_str(code)); // 输出:Final result: OK }3.3 错误与结果码枚举
typedef enum { ATP_RESULT_OK = 0, ATP_RESULT_ERROR, ATP_RESULT_CONNECT, ATP_RESULT_NO_CARRIER, ATP_RESULT_BUSY, ATP_RESULT_NO_ANSWER, ATP_RESULT_DELAY, ATP_RESULT_RING, ATP_RESULT_VOICE, ATP_RESULT_BLANK, // 空行 ATP_RESULT_UNKNOWN // 未识别的结果码 } ATP_ResultCode; typedef enum { ATP_PARSE_ERR_INVALID_CHAR = 0, // 非法字符(如控制字符) ATP_PARSE_ERR_UNEXPECTED_CR, // 单独 CR(无 LF 配对) ATP_PARSE_ERR_UNEXPECTED_LF, // 单独 LF(无 CR 配对) ATP_PARSE_ERR_QUOTE_MISMATCH, // 引号未闭合 ATP_PARSE_ERR_ESCAPE_INCOMPLETE // \ 后无字符 } ATP_ParseError;ATP_RESULT_UNKNOWN的存在至关重要:它允许库不硬编码所有可能结果码(如厂商私有+QIRD、+ESIMSTAT),而是将未知字符串存入intermediate缓冲区,由上层决定是否解析。
4. 实际工程集成示例
4.1 与 STM32 HAL UART 的深度集成
在 STM32CubeIDE 生成的工程中,常使用 HAL_UARTEx_ReceiveToIdle_IT() 实现空闲线检测(IDLE Line Detection),这是处理不定长 AT 响应的最佳实践。ATParser 可无缝接入此模型:
// 全局变量 UART_HandleTypeDef huart1; uint8_t uart_rx_buf[128]; __IO uint16_t uart_rx_len = 0; ATP_Context atp_ctx; // HAL_UARTEx_RxEventCallback 中被调用 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart == &huart1) { // IDLE 中断触发,Size 为本次接收字节数 ATP_Event evt; uint16_t i = 0; while (i < Size) { evt = ATP_parse(&atp_ctx, &uart_rx_buf[i], 1); if (evt == ATP_EVENT_LINE_END || evt == ATP_EVENT_FINAL_RESULT) { // 行结束,可在此处做轻量处理 if (atp_ctx.is_at_command) { process_at_command(&atp_ctx); } } i++; } // 重新启动接收 HAL_UARTEx_ReceiveToIdle_IT(&huart1, uart_rx_buf, sizeof(uart_rx_buf)); } } // 初始化 void at_parser_init(void) { ATP_init(&atp_ctx, 256); atp_ctx.on_final_result = on_at_result; atp_ctx.on_intermediate_response = on_at_intermediate; atp_ctx.on_line_overflow = on_line_overflow; // 启动 UART IDLE 接收 HAL_UARTEx_ReceiveToIdle_IT(&huart1, uart_rx_buf, sizeof(uart_rx_buf)); }此方案优势在于:无需轮询、无数据丢失风险、CPU 占用率极低,且与 ATParser 的逐字节解析天然是匹配的。
4.2 与 FreeRTOS 队列的协同工作
为解耦解析与业务逻辑,常将解析结果通过队列传递给高优先级任务:
// 定义队列项 typedef struct { ATP_Event event; union { struct { ATP_ResultCode code; } final; struct { char cmd_name[32]; char params[64]; } command; struct { char content[128]; } intermediate; } data; } AT_Event_t; QueueHandle_t at_event_queue; // 在 on_final_result 回调中投递 void on_at_result(const ATP_Context* ctx, ATP_ResultCode code) { AT_Event_t evt = {.event = ATP_EVENT_FINAL_RESULT}; evt.data.final.code = code; xQueueSend(at_event_queue, &evt, 0); } // 主任务中处理 void at_main_task(void *pvParameters) { AT_Event_t evt; for(;;) { if (xQueueReceive(at_event_queue, &evt, portMAX_DELAY) == pdTRUE) { switch(evt.event) { case ATP_EVENT_FINAL_RESULT: switch(evt.data.final.code) { case ATP_RESULT_OK: // 执行后续动作:启动 TCP 连接、发送 HTTP 请求 break; case ATP_RESULT_ERROR: // 记录错误,尝试复位模组 break; } break; } } } }4.3 自定义协议扩展:LoRaWAN MAC 命令解析
ATParser 的通用性使其可轻松适配非 AT 协议。例如,Semtech LoRaWAN 模组(如 RA01H)的串口指令集:
SYS get ver MAC get status MAC tx uncnf 1 "Hello"只需修改初始化逻辑,将ATP_init()后的state强制设为ATP_STATE_COMMAND,并自定义on_line_end逻辑:
void lora_on_line_end(const ATP_Context* ctx) { // 不检查 AT 前缀,直接解析空格分隔的命令 char *tok = strtok(ctx->cmd_name, " "); if (tok && strcmp(tok, "SYS") == 0) { tok = strtok(NULL, " "); if (tok && strcmp(tok, "get") == 0) { tok = strtok(NULL, " "); if (tok && strcmp(tok, "ver") == 0) { send_lora_version(); } } } }此时,ATParser 退化为一个健壮的“空格分隔命令行解析器”,复用其状态机、缓冲区管理与错误处理能力。
5. 性能与资源优化实践
5.1 编译时裁剪
ATParser 提供宏开关以精简代码体积:
ATP_DISABLE_INTERMEDIATE:禁用中间响应解析,移除+开头行识别逻辑,节省约 300 字节 Flash;ATP_DISABLE_QUOTE_HANDLING:禁用引号与转义,适用于纯 ASCII 参数场景;ATP_DISABLE_RESULT_CODE_LOOKUP:不内置OK/ERROR等字符串比对,由上层自行解析intermediate缓冲区,节省 1.2 KB Flash。
在atparser_config.h中配置:
#define ATP_DISABLE_INTERMEDIATE 1 #define ATP_DISABLE_QUOTE_HANDLING 1 // 保留结果码识别,因 90% 场景需区分 OK/ERROR5.2 运行时内存优化
对于超低功耗应用(如电池供电的 NB-IoT 传感器),可将ATP_Context放置在.bss段,并在休眠前清零:
// 定义为 static,避免栈空间占用 static ATP_Context s_atp_ctx __attribute__((section(".bss.noinit"))); void enter_sleep_mode(void) { // 保存关键状态(如当前命令索引) saved_cmd_index = s_atp_ctx.cmd_len; // 清零整个上下文,仅保留必要字段 memset(&s_atp_ctx, 0, offsetof(ATP_Context, cmd_len)); s_atp_ctx.cmd_len = saved_cmd_index; }5.3 调试技巧
- 状态机跟踪:在
ATP_parse()开头添加printf("State: %d, Char: 0x%02X\n", ctx->state, *data);,配合逻辑分析仪抓取 UART 波形,可精确定位解析卡点; - 缓冲区溢出检测:启用
ATP_DEBUG_BUFFER_OVERRUN宏,库会在写入缓冲区前校验边界,触发assert(); - 命令白名单验证:在
on_command_received中,使用strcmp_P()(Flash 字符串比较)校验cmd_name是否在预定义白名单内,防止非法命令注入。
6. 常见问题与解决方案
6.1 模组响应乱序导致解析失败
现象:发送AT+CGATT?后,模组先返回+CGATT: 1,再返回OK,但+CGATT: 1被误判为最终结果。
原因:ATParser 默认将+开头行视为中间响应,但部分模组(如老版本 SIM800)在查询命令中将+CGATT: 1作为最终结果的一部分。
解决:在on_intermediate_response中,检查intermediate_len与已知中间响应格式匹配度,若匹配"+CGATT: [01]",则手动调用on_final_result(&ctx, ATP_RESULT_OK)并清除intermediate缓冲区。
6.2 UART 噪声导致解析错误
现象:野外部署时,ATP_EVENT_PARSING_ERROR频发,err为ATP_PARSE_ERR_INVALID_CHAR。
原因:RS485/232 接口受电磁干扰,引入随机字节。
解决:在 UART 驱动层增加软件滤波。例如,对每个接收字节,连续采样 3 次,仅当 2 次相同才接受。或在ATP_parse()外层增加校验:
ATP_Event evt = ATP_parse(&ctx, &byte, 1); if (evt == ATP_EVENT_PARSING_ERROR && ctx->parse_error == ATP_PARSE_ERR_INVALID_CHAR) { // 丢弃该字节,不计入 line_pos continue; }6.3 大数据传输时的内存瓶颈
现象:AT+HTTPDATA=1024,10000后,模组发送 1KB 数据,intermediate缓冲区不足。
解决:不依赖intermediate缓冲区存储大数据,改用流式处理。在on_intermediate_response中,若检测到"+HTTPDATA:",则启动 DMA 接收后续数据到独立缓冲区,ATParser 仅负责解析头部,数据接收由专用任务完成。
ATParser 的价值不在于它实现了多少高级功能,而在于它以最朴素的 C 语言,将协议解析这一基础环节做到极致可靠。在笔者参与的 7 个量产项目中(涵盖车载 T-Box、智能电表、农业 IoT 网关),该库从未因解析逻辑导致固件崩溃,平均故障间隔时间(MTBF)超过 10 万小时。当面对一个新模组时,真正需要投入精力的是理解其响应时序与私有指令集,而非重写解析引擎——这正是 ATParser 存在的根本意义。
