一、完整代码

程序设计思路
发送 AT 指令后,程序会等待模块回复,有一个超时时间,并通过返回值判断指令是否发送成功。
二、详细概念解释
cs
/* USER CODE BEGIN Application */
// 原有函数...
// ================================================================================
// 函数名: AT_SendCmd
// 功能描述: 发送AT指令给WiFi/蓝牙模块,并等待模块回复"OK"
// 这是一个"阻塞式"函数,在收到回复或超时之前,不会返回
//
// 输入参数:
// cmd -> AT指令字符串 (例如 "AT" 或 "AT+CWMODE=1")
// timeout_ms -> 超时时间,单位毫秒 (例如 1000 表示最多等1秒)
//
// 返回值: 0 -> 成功 (收到了模块回复的 "OK")
// -1 -> 失败 (超时了,模块没理我们)
// -2 -> 失败 (你传进来的指令是空的,或者是NULL指针)
// -3 -> 失败 (模块回复了 "ERROR",说明指令格式错了)
// ================================================================================
int AT_SendCmd(char *cmd, int timeout_ms)
{
// ==============================================
// 第一步:入参检查 (Defensive Programming)
// ==============================================
// 检查指针是否为空,或者字符串长度是否为0
// 防止程序 crash(崩溃)
if(cmd == NULL || strlen(cmd) == 0)
{
return -2; // 返回错误码-2,表示参数无效
}
// ==============================================
// 第二步:定义局部变量
// ==============================================
uint8_t rec_data; // 临时变量:用来存从FIFO读出的1个字节
char rec_buf[128] = {0}; // 接收缓冲区:用来存完整的应答字符串,初始化为全0
uint16_t rec_idx = 0; // 索引:记录当前存到缓冲区第几个位置了
uint32_t start_tick = xTaskGetTickCount(); // 记录当前的系统时间(心跳数)
// ==============================================
// 第三步:清空旧的接收FIFO
// ==============================================
// 这是一个空循环,作用是把FIFO里残留的旧数据全部读出来扔掉
// 避免上一次指令的回复干扰这一次的判断
while(UART_FIFO_ReadByte(&uart1_rx_fifo, &rec_data));
// ==============================================
// 第四步:发送AT指令
// ==============================================
// 先发送你传进来的指令内容 (比如 "AT")
USART1_SendStr(cmd);
// 自动补回车换行符 (\r\n)
// 因为所有AT指令都必须以回车结尾,模块才能识别
USART1_SendStr("\r\n");
// ==============================================
// 第五步:超时循环,等待应答
// ==============================================
// 循环条件:(当前时间 - 开始时间) < 超时时间
// pdMS_TO_TICKS(): 这是FreeRTOS的宏,把毫秒转换成系统心跳数(Ticks)
while((xTaskGetTickCount() - start_tick) < pdMS_TO_TICKS(timeout_ms))
{
// 内层循环:拼命读取FIFO里的数据
// 只要FIFO里有数据,就读出来
while(UART_FIFO_ReadByte(&uart1_rx_fifo, &rec_data))
{
// 保护机制:防止存的数据太多,溢出缓冲区
// sizeof(rec_buf)-1 是为了给字符串结束符 '\0' 留位置
if(rec_idx < sizeof(rec_buf)-1)
{
rec_buf[rec_idx++] = rec_data; // 把读到的字节存入缓冲区
rec_buf[rec_idx] = '\0'; // 手动在末尾加一个结束符,保证它是合法的C语言字符串
}
// 【核心逻辑】检查是否收到 "OK"
// strstr(): C语言标准库函数,在 rec_buf 里找 "OK" 这两个字
// 如果找到了,说明指令执行成功,我们可以凯旋了!
if(strstr(rec_buf, "OK") != NULL)
{
// 调试用:把完整的应答打印到电脑串口
printf("应答: %s\r\n", rec_buf);
return 0; // 返回0,表示成功,函数直接结束
}
// 【附加逻辑】检查是否收到 "ERROR"
// 如果收到ERROR,说明指令发错了,不用等超时了,直接退出
if(strstr(rec_buf, "ERROR") != NULL)
{
printf("指令错误: %s\r\n", rec_buf);
return -3; // 返回-3,表示收到错误回复
}
}
// 【非常重要】延时10毫秒
// 如果不加这句,这个while循环会以100%的速度跑,把CPU占满
// 加了 vTaskDelay,FreeRTOS就能切换去干别的事(比如刷新OLED)
vTaskDelay(pdMS_TO_TICKS(10));
}
// ==============================================
// 第六步:超时处理
// ==============================================
// 如果代码跑到了这里,说明上面的while循环结束了,也就是时间到了还没收到OK
printf("指令超时: %s\r\n", cmd);
return -1; // 返回-1,表示超时失败
}
/* USER CODE END Application */
1. 什么是"入参检查"?
入参检查(Parameter Checking),就是在函数刚开始时,先校验用户传入的参数是否合法。
- 为什么要做?
- 防止程序崩溃:比如用户传了 NULL,调用 strlen(NULL) 会导致崩溃(HardFault)。
- 防止逻辑错误:传入空字符串 "",模块不会响应,直接报错更好。
- 这是一种职业习惯:假设调用者总会出错,提前处理各种异常,代码更健壮。
- 代码示例:
if(cmd == NULL || strlen(cmd) == 0)
这行意思是:"如果指针是空的或字符串长度为0,直接返回错误。"
2. 什么是 xTaskGetTickCount()?
- Tick(心跳):FreeRTOS 系统定时器每隔固定时间(如1ms)中断一次,计数值加1,这就是 Tick。
- xTaskGetTickCount():获取当前系统 Tick 数。
- 计时用法:
- 开始前记录:start_tick = 当前时间
- 过程中计算:当前时间 - start_tick
- 如果差值大于设定的 timeout_ms,说明超时。
3. 什么是 strstr()?
- 作用:在一个字符串中查找特定的"关键词"。
- 原型:strstr(大海, 针);
- 返回值 :
- 找到:返回指向关键词第一个字符的指针(非NULL)
- 没找到:返回 NULL
- 代码示例:
if(strstr(rec_buf, "OK") != NULL)
表示"如果在 rec_buf 里找到了 OK,说明指令成功。"
4. 为什么最后要加 vTaskDelay(10)?
- 如果不加 :
- while循环会疯狂占用CPU,导致系统卡死,其他任务得不到运行时间。
- 加了以后 :
- vTaskDelay(pdMS_TO_TICKS(10)) 表示"休息10毫秒,给其他任务让出CPU",系统运行更流畅。
三、函数流程总结
- 安检:检查参数合法性。
- 清场:清除上次残留的数据。
- 喊话:通过串口发送指令。
- 等待:监听接收缓冲区(FIFO)。
- 判断 :
- 收到 "OK" → 成功
- 收到 "ERROR" → 失败
- 超时未收到 → 超时失败

发送指令超时时间设置建议
发送指令的超时时间,应大于一次阻塞的时间,否则可能会导致任务还未得到响应就已经判定超时。

如果阻塞时间设置为10,且超时时间也是10ms,就会出现超时问题。

因为你给定超时时间10ms,但任务本身阻塞了10 Tick,必定会超时。
ESP8266 回复举例
你发送:
AT\r\n
ESP8266 的标准回复:
AT\r\n ← 回显:模块先原样返回你发的内容
\r\nOK\r\n ← 应答:真正的执行结果
所以 rec_buf 里存的是:
"AT\r\n\r\nOK\r\n"
打印出来效果:
AT ← 回显
← 空行(两个 \r\n)
OK ← 真正的OK
中间多了一行空行(因为有两个连续的 \r\n)。可以通过关闭 ESP32 的回显来解决这个问题。

我们还可以使用信号量(Semaphore)来保证数据的完整性。

这样就可以了,但缓冲区依然可能有两个脏数据,不过一般不影响正常使用。
四、问题解析
为什么 strstr(..., "OK") != NULL 能跳过前面的回显?
1. strstr 的原理
strstr 会在整个字符串中查找目标子串(如 "OK"),只要存在就返回指向 "O" 的指针,否则返回 NULL。
2. 实例解析
rec_buf 可能内容如下:
内存: 'A' 'T' '\r' '\n' '\r' '\n' 'O' 'K' '\r' '\n' '\0'
地址: 0x100 ... 0x10A
执行:
char *p = strstr(rec_buf, "OK");
此时 p 指向 'O',即 0x106,p != NULL 成立。
打印 p:
printf("%s\r\n", p);
会输出:
OK
这样就跳过了前面的回显和空行。
五、FreeRTOS延时与超时机制
1. 代码片段回顾
while((xTaskGetTickCount() - stasic_tick) < pdMS_TO_TICKS(timeout_ms))
{
// 读FIFO数据...
vTaskDelay(5);
}
2. 时间片与超时关系
- FreeRTOS 的 vTaskDelay(n) 表示至少阻塞 n 个 Tick,不是精确延时。
- 设 configTICK_RATE_HZ = 1000,即 1 Tick = 1ms。
情况1:超时时间 ≤ vTaskDelay 时间
- 任务阻塞时间等于或大于超时时间,醒来时直接超时,可能还没来得及处理数据。
情况2:超时时间 > vTaskDelay 时间
- 任务有多次机会醒来处理数据,能正常获取模块回复。
3. 多任务调度影响
- vTaskDelay(5) 结束后,任务不一定马上运行,可能被高优先级任务抢占,实际阻塞时间可能更长。
4. 最佳实践建议
- vTaskDelay 时间建议设置为 1ms,提高响应速度。
- 超时时间建议为 vTaskDelay 的 2~5 倍,确保有足够时间处理数据。
- 也可以用 taskYIELD() 替代 vTaskDelay,只让出CPU,不阻塞固定时间。
5. 总结
|-------------------------|--------------|
| 现象 | 原因 |
| 超时时间 ≤ vTaskDelay → 超时 | 任务在休眠,醒来时已超时 |
| 超时时间 > vTaskDelay → 正常 | 任务有机会多次读取数据 |
核心公式:
超时时间 ≥ (vTaskDelay 时间 × 2) + 模块响应时间
按此设置,能有效避免因时间片分配导致的意外超时。

6. 进阶优化
还可以利用 FreeRTOS 的任务通知或事件组,进一步防止任务过度占用CPU资源,提升系统整体效率。