LV035-AT命令全部返回ERROR排查记录
接线核对过三遍、波特率没错、模块换过两个,at AT 依然一条都通不了——自测打印的 AT test success! 从来没出现过。这篇记录这个故障的完整排查过程:现象看着是"模块不听话",根因却在 HAL 的一行启动顺序上,而另外两个"小问题"在它被修掉之前根本暴露不出来。
一、现象
模块接在 USART3 上,驱动按LV025-ATK-MW8266D驱动实现的方式移植完成,编译无警告,上电后串口打印:
[ESP8266] at test ...
[ESP8266] uart: rx_err=0 rx_st=0 rx_type=0 buf=1/256 SR=0000 CR1=000C
[ESP8266] at test: no response换任意指令都一样:
at AT → no response
at AT+GMR → no response
at AT+CIFSR → no response初看像是"模块完全没回数据",但状态行里 buf=1/256 是个矛盾:缓冲区里明明有 1 个字节。如果一个字节都没收到,buf 会恒为 0;既然涨到了 1,说明数据到了、只是再也没涨上去。
这就是整个排查的转折点——问题不在模块,也不在接线,而在"收到第一个字节之后发生了什么"。
二、第一层根因:ReceiveToIdle 把自己中止了
1. 原来的写法
最初的接口层直接把接收交给 HAL,一帧收完由 HAL 回调转发长度:
// 旧实现(已废弃)
void atk_mw8266d_uart_rx_restart(void)
{
g_rx_done = 0U;
g_rx_len = 0U;
(void)HAL_UARTEx_ReceiveToIdle_IT(&huart3, g_rx_buf, ATK_MW8266D_UART_RX_BUF_SIZE);
}看起来是最标准的写法:启动"收到空闲就结束"的中断接收,HAL 在 IDLE 时调 HAL_UARTEx_RxEventCallback() 并给出长度。逻辑上没问题,问题出在这个函数内部的动作顺序。
2. HAL 内部的动作顺序
HAL_UARTEx_ReceiveToIdle_IT() 先调 UART_Start_Receive_IT() 打开中断,再清 IDLE 标志:
// flash-app/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_uart.c
HAL_StatusTypeDef UART_Start_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
huart->pRxBuffPtr = pData;
huart->RxXferSize = Size;
huart->RxXferCount = Size;
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->RxState = HAL_UART_STATE_BUSY_RX;
/* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
__HAL_UART_ENABLE_IT(huart, UART_IT_ERR); /* ← 先开中断 */
/* Enable the UART Data Register not empty Interrupt */
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);
return HAL_OK;
}// 同一个文件, HAL_UARTEx_ReceiveToIdle_IT() 尾部
status = UART_Start_Receive_IT(huart, pData, Size);
if (status == HAL_OK)
{
if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
{
__HAL_UART_CLEAR_IDLEFLAG(huart); /* ← 后清标志 */
ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);
}
else
{
/* In case of errors already pending when reception is started,
Interrupts may have already been raised and lead to reception abortion.
(Overrun error for instance).
In such case Reception Type has been reset to HAL_UART_RECEPTION_STANDARD. */
status = HAL_ERROR;
}
}那个 else 分支的注释,HAL 自己就把这个坑写在源码里了:启动接收时如果有错误标志已经挂起,中断会立刻进入,接收会被中止。
3. 时序推演
上一帧的最后一个字节还留在 DR 里没被读走(rx_restart() 只清了软件标志,没人读 DR),于是:
| 时刻 | 动作 | 后果 |
|---|---|---|
| T0 | 上一帧结束,rx_restart() 被调用 | 软件标志清零,但 DR 里还躺着上一个字节,RXNE 仍置位 |
| T1 | UART_Start_Receive_IT() 打开 ERR/RXNE 中断 | 挂起的 RXNE/ORE 立刻使能了一个待处理的错误中断 |
| T2 | 函数返回前,中断服务程序进入 HAL_UART_IRQHandler() | 检测到 ORE,执行 UART_EndRxTransfer(),关掉 RXNE/ERR 中断,ReceptionType 复位成 STANDARD |
| T3 | HAL_UARTEx_ReceiveToIdle_IT() 检查 ReceptionType | 已经不是 TOIDLE,走 else 分支 |
| T4 | 接收停止 | 再也没有 RXNE 中断,buf 停在 1 不动 |
实测症状正是"每次只收到 1 字节,之后再也收不到"——那 1 个字节就是 T0 时刻残留在 DR 里的旧数据。
4. 为什么这个坑特别隐蔽
(1)不报错。调用返回 HAL_OK,回调也没被调用,rx_err 计数还是 0——所有"看起来能反映问题"的地方都是干净的。
(2)不崩。没有 HardFault、没有断言,程序照常跑,只是 AT 指令永远等不到应答。
(3)外设配置全对。CubeMX 里 USART3 的 NVIC 勾了、引脚对了、波特率对了,SR/CR1 快照(SR=0000 CR1=000C)也看不出异常,因为中断已经被 HAL 关掉了。
结果就是"硬件全对、代码全错、现象全无"。
5. 修法:接收交给驱动自己管
既然 HAL 的接收状态机会因为一个残留字节把自己干掉,最直接的办法是不再经过它——RXNE 来了自己读 DR,IDLE 来了自己判帧:
// flash-app/Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.c
void atk_mw8266d_uart_irq_handler(void)
{
uint32_t sr = huart3.Instance->SR;
uint8_t byte;
if ((sr & USART_SR_ORE) != 0U) /* 溢出: 上一个字节还没读走就又来了一个 */
{
(void)huart3.Instance->SR; /* 先读 SR 再读 DR 才能清 ORE */
(void)huart3.Instance->DR; /* 这一次读 DR 已经把字节消耗掉了 */
g_rx_errors++;
/* ORE 与 RXNE 同时置位是常态, 上面读 DR 已把 RXNE 一并清掉;
* 必须重取 SR, 否则下面会按旧快照再读一次 DR, 收到一个假字节 */
sr = huart3.Instance->SR;
}
if ((sr & USART_SR_RXNE) != 0U) /* 收到一个字节 */
{
byte = (uint8_t)(huart3.Instance->DR & 0xFFU);
if (g_rx_done != 0U)
{
g_rx_errors++; /* 上一帧还没被取走: 新数据只能丢 */
}
else if (g_rx_len < ATK_MW8266D_UART_RX_BUF_SIZE)
{
g_rx_buf[g_rx_len] = byte;
g_rx_len++;
}
else
{
g_rx_errors++; /* 一帧超过缓冲: 多出来的丢掉 */
}
}
if ((sr & USART_SR_IDLE) != 0U) /* 总线空闲: 一帧结束 */
{
(void)huart3.Instance->SR;
(void)huart3.Instance->DR; /* 清 IDLE, 顺序不能反 */
g_rx_buf[g_rx_len] = '\0'; /* 便于上层直接当字符串用 */
g_rx_done = 1U;
}
}配套动了四处,缺一不可:
| 位置 | 改动 | 原因 |
|---|---|---|
atk_mw8266d_uart_init() | 先空读一遍 SR/DR,再使能 RXNE/IDLE | 上电残留的标志会立刻制造一次假事件 |
USART3_IRQHandler() | 先调 atk_mw8266d_uart_irq_handler(),再调 HAL_UART_IRQHandler() | 标志被自己清干净后,HAL 查不到事件会直接返回 |
usart.c | 删除 USART3 的 RxCplt / RxEvent / Error 三个回调 | 两套接收逻辑并存会互相踩缓冲 |
rx_restart() | 只清长度与完成标志,不再重新启动 HAL 接收 | 中断自始至终开着,不存在"被停掉"的状态 |
现在这个 bug 在头文件里留了一句注释,防止以后又改回 ReceiveToIdle:
// flash-app/Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.h
* 接收中断由本层自己处理(atk_mw8266d_uart_irq_handler), 不经过 HAL 的
* ReceiveToIdle: HAL 那一套是"先开中断再清标志", 上一帧残留在 DR 里的字节
* 会立刻触发 ORE, HAL 随即 EndRxTransfer 把刚启动的接收中止掉却仍返回
* HAL_OK, 结果是接收静默失效(实测症状: 每次只收到 1 字节, 之后再也收不到)。三、第二层:尾随空格
接收通了之后,at AT 能返回 OK 了,但 at AT+CWMODE? 这类带问号的指令仍然返回 ERROR。原因不在模块,在命令行本身:串口助手下发时多敲了一个空格,而空格被原样拼进了发给模块的指令里。
// flash-app/Core/Src/usart.c
if ((byte == (uint8_t)'\r') || (byte == (uint8_t)'\n')) /* 行尾: 与命令表比对 */
{
/* 行尾空白一律裁掉: 'reboot ' 也要能命中, 更要紧的是 'at AT+CWMODE? '
* 这类尾随空格会让 ESP8266 把整条指令判成非法, 直接回 ERROR */
while ((g_cmd.len > 0U) &&
((g_cmd.line[g_cmd.len - 1U] == ' ') || (g_cmd.line[g_cmd.len - 1U] == '\t')))
{
g_cmd.len--;
}这一层只有在第一层修好之后才看得见:接收都没通的时候,所有指令都是"无应答",分不清是没收到还是被判非法。
四、第三层:等待窗口与喂狗
前两层修完,快指令正常了,慢指令又开始出问题:
(1)at 命令原先只等 500 ms,而 AT+CWJAP(连接 AP)官方建议等到 15 s,AT+RST 也要好几秒——这类指令必然超时,表现又变成"返回错误"。
(2)把窗口提到 15 s 之后,等待期间主循环被占住,3 s 的 IWDG 会把板子复位。于是要在等待循环里喂狗,而喂狗又踩到第二个坑:MX_IWDG_Init() 在这个工程里是空操作,看门狗启动前 hiwdg.Instance 还是 NULL,直接调 HAL_IWDG_Refresh() 会向地址 0 写 KR,触发 HardFault——初始化阶段的 AT 等待直接死在自测之前。
两处改动的最终形态:
// flash-app/Drivers/BSP/ATK-MW8266D/atk_mw8266d_test.c
/* 'at' 命令等待应答的最长时间: 收到 OK/ERROR/FAIL/> 会提前返回, 这里只是上限。
* 取 15s 是为了容得下 AT+CWJAP(连接 AP, 官方建议 15s)与 AT+RST 这类慢指令;
* 快速指令在应答到达时就返回, 不会真的等满。等待期间由驱动自己喂狗 */
#define ATK_TEST_AT_CMD_WINDOW_MS 15000U// flash-app/Core/Src/iwdg.c
/* 喂狗: 看门狗未使能时 hiwdg.Instance 仍为 NULL(MX_IWDG_Init 是空操作),
* 此时直接刷新会向地址 0 写 KR 而触发 HardFault, 所以先判空。
* 初始化阶段(含 ESP8266 的 AT 等待)也会调用本函数, 必须走这条判空路径 */
void iwdg_feed(void)
{
if (hiwdg.Instance != NULL)
{
(void)HAL_IWDG_Refresh(&hiwdg);
}
}「窗口取上限而不是定值」这一点值得强调:15 s 是不会"真的等 15 s"的,send_raw() 在收到 OK/ERROR/FAIL/> 时就返回,所以把上限开大只影响最坏情况,不影响正常指令的响应速度。
五、修完之后的形态
| 层次 | 修复前 | 修复后 |
|---|---|---|
| 接收 | HAL ReceiveToIdle,被残留字节的 ORE 中止,只收到 1 字节 | 驱动自读 SR/DR,IDLE 判帧,中断常开 |
| 中断挂接 | 依赖 HAL 回调转发 | USART3_IRQHandler 先调驱动处理函数 |
| 命令行 | 尾随空格原样下发 | 行尾空白裁掉 |
| 长指令 | 单行 16 字节,长指令被截断 | 单行 64 字节,参数在中断里做快照 |
| 等待 | 500 ms 窗口 | 15 s 上限,命中应答即返回 |
| 喂狗 | 等待期间不喂,长等待会被 IWDG 复位 | iwdg_feed() 判空后刷新 |
| 出错信息 | 只打印"失败" | 打印 rx_err/RxState/ReceptionType/buf/SR/CR1 |
修完的验证方式很朴素:at AT、at AT+GMR、at AT+CIFSR、at AT+CWMODE? 逐条试通,再试一条 at AT+CWJAP="ssid","pwd" 确认 15 s 窗口与喂狗都生效。
六、复盘
(1)"没收到数据"和"收到一个字节"是两个完全不同的问题。buf=1/256 这一栏把故障范围从"整条链路"一步缩小到"接收中间层",比换模块、换波特率快得多。排错时先找一个能量化的观测点,再去猜原因。
(2)HAL 的注释是文档,不是装饰。ReceptionType 那个 else 分支把"启动接收时错误标志已挂起"的情况写得清清楚楚,读一遍源码就省掉半天。
(3)残留状态要先清再开中断。无论是 DR 里的旧字节还是 SR 里挂起的 ORE,只要在开中断之前不处理,第一个中断就一定是个假事件。
(4)多层故障会互相掩盖。三个问题叠在一起时,现象只有"所有指令都错";修掉最外层那个,下面两个才现形。这也说明一开始就该把 SR/CR1/buf 打出来,而不是在"全错"这一层反复试。