# at_client **Repository Path**: szwht/at_client ## Basic Information - **Project Name**: at_client - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-17 - **Last Updated**: 2026-10-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # AT Client 驱动 (STM32F103RCT6 + RT-Thread Nano + LL 库) 面向 STM32F103 通用 AT 模组的轻量 AT 命令/URC 框架。 - 底层: LL 库 + USART DMA + 空闲中断(IDLE) + 环形缓冲, CPU 占用低 - 协议层: 命令串行化、关键字响应匹配、超时控制 - 等待窗口: `at_send_wait` 可携带命令或纯等待同步等待关键字/URC 上报(可截获) - URC: 前缀注册表分发到用户回调, 与命令响应互不干扰 - 仅依赖 RT-Thread 信号量/线程, 无堆依赖(全静态对象), 适配 Nano ## 1. 文件结构 ``` at_client/ +-- inc/ | +-- at_config.h 硬件映射与参数配置(改硬件只改这里) | +-- at_port.h 底层移植接口 | +-- at_client.h 协议层 API(命令发送/URC 注册) +-- src/ | +-- at_port.c LL+DMA+IDLE 中断+环形缓冲实现 | +-- at_client.c 行解析、URC 分发、命令状态机 +-- README.md ``` ## 2. 硬件连接与资源占用 | 项目 | 配置 | 说明 | |---|---|---| | 串口 | USART2 | TX = PA2, RX = PA3 | | DMA TX | DMA1_Channel7 | 普通模式, TC 中断通知完成 | | DMA RX | DMA1_Channel6 | 普通模式, IDLE/TC 中断搬运数据 | | 波特率 | 115200, 8N1 | `AT_UART_BAUD_RATE` | 中断服务函数已在 `at_port.c` 中实现: `USART2_IRQHandler` / `DMA1_Channel6_IRQHandler` / `DMA1_Channel7_IRQHandler`。 若工程中(`stm32f1xx_it.c` 等)已有同名函数, 请删除原生桩函数, 或将 `at_config.h` 中的 `AT_*_IRQHandler` 宏重命名后自行转发。 ## 3. 工程集成步骤 1. 将 `inc/` 与 `src/` 加入工程编译, 并把 `inc/` 加入头文件搜索路径。 2. **必须在工程全局宏中定义 `USE_FULL_LL_DRIVER`** (`LL_USART_Init` / `LL_USART_InitTypeDef` 等由该宏使能): - Keil: Options -> C/C++ -> Define 添加 `USE_FULL_LL_DRIVER` - GCC/Makefile: `CFLAGS += -DUSE_FULL_LL_DRIVER` - SCons: `env.Append(CPPDEFINES = ['USE_FULL_LL_DRIVER'])` 3. 确保以下 LL 源文件在编译列表中(通常 CubeMX 生成后已存在): `stm32f1xx_ll_usart.c`、`stm32f1xx_ll_dma.c`、 `stm32f1xx_ll_gpio.c`、`stm32f1xx_ll_bus.c` 4. rtconfig.h 需开启: `RT_USING_SEMAPHORE`、`RT_USING_THREAD` (Nano 默认已开启, 无需 rt_malloc)。 5. SConscript 参考(可选): ```python # at_client/SConscript from building import * import os cwd = GetCurrentDir() src = Glob('src/*.c') path = [os.path.join(cwd, 'inc')] group = DefineGroup('at_client', src, depend=[''], CPPPATH=path) Return('group') ``` ## 4. 关键配置(inc/at_config.h) | 宏 | 默认值 | 说明 | |---|---|---| | `AT_UART_PCLK` | 36000000 | **USART2 所在 APB1 实际时钟(Hz), 必须按系统时钟修改**。72MHz 系统 + APB1 二分频时填 36000000 | | `AT_RX_DMA_BUF_SIZE` | 256 | DMA 单批接收暂存区 | | `AT_RX_RING_BUF_SIZE` | 2048 | 环形缓冲, 高速业务可加大 | | `AT_RESP_BUF_SIZE` | 1024 | 单条命令响应缓存 | | `AT_RECV_THREAD_PRIORITY` | `RT_THREAD_PRIORITY_MAX - 3` | 接收线程优先级 | | `AT_NVIC_PRIORITY_*` | 5 | 三个中断优先级(F1 为 0~15) | | `AT_USING_DEBUG` | 关闭 | 打开后用 `rt_kprintf` 打印收发行, 便于调试 | 若串口/DMA 需换成其他外设(如 USART1 + DMA1_CH4/5), 只需修改 `at_config.h` 中的映射宏与 `AT_*_TC_FLAG/CLEAR` 宏, `at_port.c` / `at_client.c` 无需改动。 ## 5. 使用示例 ## 5.1 基础流程: 回调内轻量处理(无需信号量) URC 回调运行在 AT 接收线程上下文, 就是一次普通函数调用。打印日志、 置标志、拷贝数据、发消息队列等非阻塞操作可直接在回调里完成, 不需要 信号量; 唯一禁止的是在回调中调用 `at_send_cmd()` 等阻塞式 AT 命令 (接收线程自等待会死锁)。 ```c #include "at_client.h" /* 业务侧标志位: URC 事件通知(业务线程轮询) */ static volatile rt_uint8_t mqtt_msg_flag; /* 回调轻量处理示例 */ static void urc_ready(const char *line) /* 模组开机就绪 */ { rt_kprintf("module ready\n"); } static void urc_receive(const char *line) /* 数据到达上报 */ { rt_kprintf("urc: %s\n", line); mqtt_msg_flag = 1; /* 通知业务线程, 立即返回 */ } static const at_urc_t urc_table[] = { { "RDY", urc_ready }, { "+RECEIVE", urc_receive }, }; static void at_demo_thread(void *parameter) { /* 1. 初始化驱动: 配置串口/DMA/中断并启动接收线程 */ at_client_init(); /* 2. 注册 URC 表(表可用 static const, 生命周期覆盖运行期) */ at_urc_table_set(urc_table, sizeof(urc_table) / sizeof(urc_table[0])); /* 3. 等待模组上电就绪(也可改为 at_send_wait(RT_NULL, "RDY", 5000) 同步等待) */ rt_thread_mdelay(3000); /* 4. 关闭回显: 命令发起前驱动会自动清理开机残留, 无需手动 at_flush */ at_exec_cmd("ATE0", 1000); /* 5. 发送命令并解析多行响应 */ if (at_send_cmd("AT+CSQ", "+CSQ", 2000) == AT_OK) { const char *line = at_resp_line(at_get_resp(), 0); /* "+CSQ: 25,0" */ rt_kprintf("csq: %s\n", (line != RT_NULL) ? line : "(none)"); } /* 超时/失败无需手动 at_flush: 下一条命令发起前会自动清理残留 */ /* 6. 业务循环 */ while (1) { if (mqtt_msg_flag) { mqtt_msg_flag = 0; /* 业务线程里可安全调用 AT 命令 */ } rt_thread_mdelay(50); } } ``` ## 5.2 URC 后需联动 AT 命令时: 转交业务线程 如果 URC 处理逻辑中需要发送 AT 命令(例如收到 "+RECEIVE" 后要发 AT+QMTRECV 读取数据), 必须把事件转交业务线程执行, 否则会因接收 线程自等待而死锁。转交手段不限于信号量, 常用方案: ```c /* 方案一: 信号量, 只通知"有事件" */ static struct rt_semaphore urc_sem; static void urc_receive(const char *line) { rt_sem_release(&urc_sem); /* 只通知, 立即返回 */ } /* 业务线程: 等事件后安全执行 AT 命令 */ if (rt_sem_take(&urc_sem, rt_tick_from_millisecond(1000)) == RT_EOK) { at_send_cmd("AT+QMTRECV=0,0", RT_NULL, 2000); } /* 方案二: 消息队列, 把 URC 内容一并传给业务线程 */ static struct rt_messagequeue urc_mq; static char urc_mq_pool[8 * 128]; static void urc_receive(const char *line) { rt_mq_send(&urc_mq, (void *)line, strlen(line) + 1); /* 非阻塞, 立即返回 */ } /* 业务线程: 取事件内容 */ char buf[128]; if (rt_mq_recv(&urc_mq, buf, sizeof(buf), rt_tick_from_millisecond(1000)) == RT_EOK) { /* 解析 buf 并按需执行 AT 命令 */ } ``` 注: 信号量/消息队列对象在业务线程初始化一次即可; 回调中的 `rt_sem_release` / `rt_mq_send` 均可安全调用。 ## 5.3 两步式命令(数据模式提示符, 如 MQTT 发布) AT+QMTPUBEX / AT+QHTTPPOST / FTP 上传等命令的流程为: 发命令 -> 模组返回裸提示符 `>`(不带 CRLF) -> 发送负载数据 -> 返回 OK。 裸提示符的字节级检测由驱动完成(普通行匹配不支持无换行字符): ```c /* 阶段1: 发命令, 等待 '>' 提示符(字节级检测, 无需行结束符); 命令支持 printf 格式化, 字面 '%' 须写成 "%%" */ int payload_len = 5; if (at_send_cmd_prompt("AT+QMTPUBEX=0,1,0,0,\"topic\",%d", ">", 1000, payload_len) == AT_OK) { /* 阶段2: 发送负载(长度须与命令参数 payload_len 一致), 等最终 OK */ if (at_send_payload("hello", 5, RT_NULL, 5000) == AT_OK) { /* 响应含 "+QMTPUBEX: 0,1,0" 结果行 */ rt_kprintf("pub resp: %s\n", at_get_resp()); } else { rt_kprintf("pub failed\n"); /* 残留由下一条命令发起前自动清理 */ } } ``` 注意: 1. `at_send_cmd_prompt` 成功后进入数据会话, **必须**接着调用 `at_send_payload` 结束会话并释放命令锁, 否则其他线程的 AT 命令会 返回 `AT_ERR_BUSY`。 2. 数据会话期间命令锁被持有, 不要再由其他线程发起 AT 命令。 3. 阶段1 超时(模组未给 `>` )时无需手动 `at_flush()`, 下一条命令发起 前会自动清理; 阶段2 的 payload 长度必须与命令声明的长度严格一致, 否则模组无法退出数据模式。 ## 5.4 同步等待关键字/截获 URC: at_send_wait 在超时内等待某条接收行出现(子串匹配), 命中立即返回 `AT_OK` 并可 读取命中行; `cmd` 非空时先自动补 CRLF 发送命令再等待。适合上电 等待模组就绪、发命令后等异步到达的关键字(如 MQTT 接收)、透传发 送后等响应、或在业务线程同步等一条 URC: ```c /* 上电等待模组就绪(不发送命令, 最多 5s) */ if (at_send_wait(RT_NULL, "RDY", 5000) == AT_OK) { rt_kprintf("module ready: %s\n", at_get_resp()); /* "RDY" */ } /* 发命令后等待关键字(如 MQTT 消息异步下发, 最多 60s) */ if (at_send_wait("AT+QMTRECV=0,0", "+QMTRECV", 60000) == AT_OK) { /* 命中行已缓存, 可解析处理 */ } /* 透传发送后等待响应关键字 */ at_send_raw((const rt_uint8_t *)"hello", 5, 1000); if (at_send_wait(RT_NULL, "+RECEIVE", 3000) == AT_OK) { /* 命中行已缓存, 可解析处理 */ } ``` 注意: 1. 命中行**优先于 URC 表被截获**: 若等待的关键字已注册 URC, 该行 不再触发 URC 回调; 等待期间其他 URC 行仍正常分发。 2. 窗口开启时会清空接收残留(与命令一致), 只认窗口开启后新到的 行; 若数据可能先于等待到达, 请改用 URC 回调。 3. 等待期间持有命令锁, 其他线程发起的 AT 命令返回 `AT_ERR_BUSY`。 4. 仅关键字命中才结束等待, 期间收到的 OK 等收尾行不影响(适配关键 字异步迟于 OK 到达的场景); 携带 `cmd` 时若模组直接回复 ERROR/FAIL, 则提前返回 `AT_ERR_ERROR`; 无命中则超时返回 `AT_ERR_TIMEOUT`。 ## 6. API 速查(inc/at_client.h) | 函数 | 说明 | |---|---| | `at_client_init()` | 初始化串口+接收线程, 上电调用一次 | | `at_urc_table_set(table, size)` | 注册/替换 URC 表(前缀匹配) | | `at_send_cmd(cmd_expr, resp, timeout, ...)` | 发命令等关键字响应; `resp=NULL` 等 `OK`, 指定 `resp` 未命中仅得 `OK` 时返回 `AT_ERR_NODATA`; 支持 printf 格式化(无参数即普通命令), 如 `at_send_cmd("AT+CGDCONT=1,\"IP\",\"%s\"", NULL, 1000, "cmnet")` | | `at_exec_cmd(cmd_expr, timeout, ...)` | 只等 OK/ERROR 的简化版, 支持 printf 格式化(无参数即普通命令), 如 `at_exec_cmd("AT+CGDCONT=1,\"IP\",\"%s\"", 1000, "cmnet")` | | `at_send_wait(cmd_expr, keyword, timeout, ...)` | 同步等待接收行命中关键字; `cmd_expr` 非空时先发命令再等待(命中行优先于 URC 被截获); 支持 printf 格式化 | | `at_send_cmd_prompt(cmd_expr, prompt, timeout, ...)` | 两步式命令: 等裸提示符(如 '>'), 成功后进入数据会话; 支持 printf 格式化(无参数即普通命令) | | `at_send_payload(data, len, resp, timeout)` | 数据会话: 发送负载并等最终响应, 结束会话并释放锁 | | `at_send_raw(data, len, timeout)` | 原始直发(透传/数据模式), 不等待响应 | | `at_get_resp()` | 取最近一次命令的完整响应文本 | | `at_resp_line(resp, n)` | 按行(0 起)取响应 | | `at_flush()` | 可选: 立即清理接收残留(常规流程自动清理, 无需手动调用) | 返回码: `AT_OK(0)`、`AT_ERR_TIMEOUT`、`AT_ERR_ERROR`、`AT_ERR_PARAM`、 `AT_ERR_BUSY`、`AT_ERR_SEND`、`AT_ERR_NODATA`。 响应匹配规则(按到达顺序): 命中 `ERROR`/`FAIL` -> 失败; 命中 `resp` 关键字 -> 成功; `resp` 为空时命中 `OK` -> 成功; 指定了 `resp` 但只等到 `OK` -> `AT_ERR_NODATA`: 命令已执行成功, 但未返回期望数据行; 响应文本仍完整保存在 `at_get_resp()` 中, 如需兼容旧行为可按成功处理。 ## 7. 工作机制简述 ``` 接收: USART2_RX --DMA1_CH6--> rx_dma_buf --IDLE/TC 中断--> 环形缓冲 --at_recv 线程--> 行重组 --> 等待窗口命中 --> 唤醒 at_send_wait \--> URC 前缀表匹配 --> 用户回调 \--> 命令响应匹配 --> 唤醒 at_send_cmd 发送: at_send_cmd --加锁组包(自动补 CRLF)--> tx_dma_buf --DMA1_CH7--> USART2_TX --DMA TC 中断--> 等待 USART TC(移位完成) --> 返回 ``` - 普通模式 DMA + IDLE 切帧: 每次 IDLE 中断搬运一批数据并重启 DMA; DMA 缓冲满时由 TC 中断兜底搬运。 - 行重组兼容 `\r\n` / `\n` / `\r` 三种结尾; 超长行整行丢弃, 防止半行数据参与匹配造成误判。 - 命令与 `at_send_wait` 每次发起前会自动清空环形缓冲/信号量残留, 保证响应对应关系, 常规流程无需手动调用 `at_flush()`。 ## 8. 注意事项与 FAQ 1. **URC 回调上下文**: 回调在接收线程中执行, 不可阻塞、不可调用 `at_send_cmd()`(接收线程自等待会死锁)。正确做法是把事件通过 信号量/消息队列转交业务线程后再操作 AT。 2. **命令回显**: 建议初始化后执行 `ATE0` 关闭回显; 否则命令回显行 会进入响应缓存(通常不影响 OK/ERROR 判断, 但会干扰关键字匹配)。 3. **超时恢复**: 命令超时/失败后无需手动清理, 下一条命令发起前驱动 会自动清空接收与匹配残留, 无命令窗口期间的迟到数据也会被丢弃 (URC 除外, 仍正常回调)。`at_flush()` 仅适用于需要"立即清空"的 可选场景。 4. **`AT_UART_PCLK` 必须正确**, 否则波特率偏差导致乱码。该值取 USART2(APB1)时钟, 与 `LL_USART_Init` 内部时钟推导无关。 5. **环形缓冲溢出**: 缓冲满时丢弃新数据(`at_config.h` 中可加大 `AT_RX_RING_BUF_SIZE`); 高速持续数据流(如透传)建议 >= 4096。 6. **与模组断电/复位配合**: 模组重启后重新执行 `ATE0` 等初始化命令 即可(发起前自动清理重启期间残留); 如需立即清空缓冲可调用 `at_flush()`。 7. **多线程并发发命令**是安全的: 内部命令锁会串行化; 拿不到锁的 调用返回 `AT_ERR_BUSY`。