Skip to content

LV025-ATK-MW8266D驱动实现 ​

模块的接线与 CubeMX 外设配置在LV020-ATK-MW8266D硬件连接与工程配置里已经完成,这一篇接着往下走:AT 指令怎么收发、一帧应答怎么判定结束、驱动怎么从原厂工程搬进 CubeMX 工程,以及移植时要改哪几处。

一、参考实例的程序结构 ​

1. 文件组成 ​

参考工程是"ATK-ESP8266 模块 TCP 透传实验 / 战舰 STM32F103"。与本模块相关的只有下面几个文件,其余是 LED、KEY、LCD、SYSTEM 等公共驱动:

ATK_MW8266D 驱动目录

文件职责
User/main.c时钟、串口、LCD、按键初始化,调用 demo_run()
User/demo.c业务流程编排:连 AP、连 TCP、按键交互、打印结果
Drivers/BSP/ATK_MW8266D/atk_mw8266d.c模块业务层:硬件复位时序、AT 指令封装、返回值归一
Drivers/BSP/ATK_MW8266D/atk_mw8266d.h引脚宏、错误码、对外接口声明
Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.c串口接口层:字节收发、帧边界判定(RXNE + IDLE)、格式化输出
Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.h串口实例/引脚/中断宏、缓冲尺寸、接口声明
Drivers/SYSTEM/usart/usart.cUSART1 调试口,以及 HAL_UART_MspInit() 里的 USART3 分支(引脚、时钟、NVIC、中断使能)
User/stm32f1xx_it.c中断向量表;模块的串口中断没有放在这里,而是在 atk_mw8266d_uart.c 里用宏把函数名映射成 USART3_IRQHandler

最后一行是移植到 CubeMX 工程时最容易踩的地方,第三节会专门处理。

2. 三层职责划分 ​

层文件职责明确不做
应用层demo.c决定"先连 AP 再连 TCP"这类流程,按键与结果显示不碰寄存器、不拼 AT 字符串
业务层atk_mw8266d.c复位时序、每条 AT 指令的字符串与期望应答、超时值不关心字节怎么进出串口
接口层atk_mw8266d_uart.c逐字节收发、判一帧是否结束、格式化发送不懂 AT 语义,不认识 "OK"

分层带来的直接好处:换模块只需改业务层,换 MCU 只需改接口层。

3. 实例运行流程 ​

demo_run() 的启动序列是一条线性流程,任何一步失败就进入 LED 闪烁的死循环:

c
// User/demo.c
ret  = atk_mw8266d_init(115200);                             /* GPIO + 硬复位 + UART + AT 测试 */
ret  = atk_mw8266d_restore();                                /* AT+RESTORE,等 "ready" */
ret += atk_mw8266d_at_test();                                /* AT / OK,最多重试 10 次 */
ret += atk_mw8266d_set_mode(1);                              /* AT+CWMODE=1,STA 模式 */
ret += atk_mw8266d_sw_reset();                               /* AT+RST,之后延时 1 s */
ret += atk_mw8266d_ate_config(0);                            /* ATE0,关闭回显 */
ret += atk_mw8266d_join_ap(DEMO_WIFI_SSID, DEMO_WIFI_PWD);   /* AT+CWJAP,等 "WIFI GOT IP",10 s */
ret += atk_mw8266d_get_ip(ip_buf);                           /* AT+CIFSR,从应答里抠 IP */

五个要点:

(1)atk_mw8266d_init() 内部先做硬件复位(RST 拉低 100 ms、拉高后等 500 ms),再初始化串口,最后发 AT。也就是说复位发生在串口初始化之前,此时串口还没开也不影响,因为模块这 600 ms 不会发任何东西。

(2)AT+RESTORE 会重启模块,驱动等的是启动完成后打印的 ready,超时给了 3 s。

(3)AT+RST 的应答只等 OK,但模块真正重启完成还要时间,驱动在成功分支里补了 delay_ms(1000)。这个延时是必需的,短了后面第一条指令就会丢。

(4)AT+CWJAP 的期望应答是 WIFI GOT IP 而不是 OK。这一条判据选得对:只有真正拿到 IP 才算联网成功,OK 只表示指令被接收。

(5)ret 用 += 累加,只能判断"有没有失败",判断不出"哪一步失败"。移植时建议改成逐步打印,排错成本会低一个量级。

4. 透传模式下的收发路径 ​

进入透传后,串口与 TCP 之间变成裸管道,AT 语义完全消失:

方向调用说明
MCU → 服务器atk_mw8266d_uart_printf("...")写串口即可,模块原样发到 TCP
服务器 → MCUatk_mw8266d_uart_rx_get_frame() + atk_mw8266d_uart_rx_restart()按空闲判帧取走一段,取完必须 restart

透传期间不能再发 AT 指令(会被当成数据发给服务器),唯一的出口是先发 +++ 退出。

二、驱动核心机制 ​

1. 原厂的三个骨架函数 ​

三个函数串起来就是整个驱动的模型:init() 负责把模块叫醒,at_test() 负责确认链路通,send_at_cmd() 负责一次问答。

1.1 atk_mw8266d_init() ​

c
/**
 * @brief       ATK-MW8266D初始化
 * @param       baudrate: ATK-MW8266D UART通讯波特率
 * @retval      ATK_MW8266D_EOK  : ATK-MW8266D初始化成功,函数执行成功
 *              ATK_MW8266D_ERROR: ATK-MW8266D初始化失败,函数执行失败
 */
uint8_t atk_mw8266d_init(uint32_t baudrate)
{
    atk_mw8266d_hw_init();                          /* ATK-MW8266D硬件初始化 */
    atk_mw8266d_hw_reset();                         /* ATK-MW8266D硬件复位 */
    atk_mw8266d_uart_init(baudrate);                /* ATK-MW8266D UART初始化 */
    if (atk_mw8266d_at_test() != ATK_MW8266D_EOK)   /* ATK-MW8266D AT指令测试 */
    {
        return ATK_MW8266D_ERROR;
    }

    return ATK_MW8266D_EOK;
}

顺序很清楚:硬件复位(拉低 RST 再拉高)→ 初始化与模块相连的 UART → 发一条 AT 验证通路。AT 测试成功说明模块及其通讯接口都正常,失败则后续所有操作都没有意义。

1.2 atk_mw8266d_at_test() ​

c
/**
 * @brief       ATK-MW8266D AT指令测试
 * @param       无
 * @retval      ATK_MW8266D_EOK  : AT指令测试成功
 *              ATK_MW8266D_ERROR: AT指令测试失败
 */
uint8_t atk_mw8266d_at_test(void)
{
    uint8_t ret;
    uint8_t i;

    for (i=0; i<10; i++)
    {
        ret = atk_mw8266d_send_at_cmd("AT", "OK", 500);
        if (ret == ATK_MW8266D_EOK)
        {
            return ATK_MW8266D_EOK;
        }
    }

    return ATK_MW8266D_ERROR;
}

发送 AT(函数内部自动补 \r\n),在 500 ms 内等 OK,失败重试 10 次。之所以要重试,是因为复位后模块启动需要时间,第一次探测常常落在模块还没准备好的窗口里。

2. atk_mw8266d_send_at_cmd() ​

该函数在 Drivers/BSP/ATK_MW8266D/atk_mw8266d.c 中实现,声明位于 atk_mw8266d.h:

c
uint8_t atk_mw8266d_send_at_cmd(char *cmd, char *ack, uint32_t timeout);

【函数作用】

向模块发送一条 AT 指令,并在给定时间内轮询接收帧,判断应答中是否包含期望字符串。驱动里所有业务函数(连 AP、连服务器、进透传……)都建立在它之上。

【参数含义】

  • cmd:不含 \r\n 的指令正文,函数内部自动补 \r\n
  • ack:期望在应答中出现的子串,如 "OK"、"WIFI GOT IP"、">";传 NULL 表示只发不收
  • timeout:等待时间,单位 ms(实现上是 1 ms 递减的轮询)

【返回值】

成功返回 ATK_MW8266D_EOK(0);等待期内没等到期望应答返回 ATK_MW8266D_ETIMEOUT(2)。

原厂完整实现:

c
/**
 * @brief       ATK-MW8266D发送AT指令
 * @param       cmd    : 待发送的AT指令
 *              ack    : 等待的响应
 *              timeout: 等待超时时间
 * @retval      ATK_MW8266D_EOK     : 函数执行成功
 *              ATK_MW8266D_ETIMEOUT: 等待期望应答超时,函数执行失败
 */
uint8_t atk_mw8266d_send_at_cmd(char *cmd, char *ack, uint32_t timeout)
{
    uint8_t *ret = NULL;

    atk_mw8266d_uart_rx_restart();
    atk_mw8266d_uart_printf("%s\r\n", cmd);

    if ((ack == NULL) || (timeout == 0))
    {
        return ATK_MW8266D_EOK;
    }
    else
    {
        while (timeout > 0)
        {
            ret = atk_mw8266d_uart_rx_get_frame();
            if (ret != NULL)
            {
                if (strstr((const char *)ret, ack) != NULL)
                {
                    return ATK_MW8266D_EOK;
                }
                else
                {
                    atk_mw8266d_uart_rx_restart();
                }
            }
            timeout--;
            delay_ms(1);
        }

        return ATK_MW8266D_ETIMEOUT;
    }
}

二十行代码,但三处判断都不能省:

(1)为什么发之前要 restart。模块对一条指令可能分多帧回复(例如先 \r\n,再 AT+CWJAP... 回显,再 WIFI CONNECTED、WIFI GOT IP、OK)。如果不清标志,上一帧的 finsh=1 会让本函数立刻取到旧数据,出现"指令还没发就判定成功"的假阳性。

(2)为什么要丢弃不匹配的帧。模块先回一行 \r\n 空行、或者回显指令本身,都是常态。不丢弃就会一直卡在第一帧上。

(3)超时是"轮询次数"而不是硬件定时器。timeout-- 配合 delay_ms(1) 实现,所以调用它的函数是阻塞的:join_ap 最长可能卡住主循环 10 s。放在 FreeRTOS 任务里要注意喂狗,放在裸机主循环里要注意这会中断按键响应。

也正因为"所有业务函数都压在它之上",驱动里没封装的 AT 指令,只要照着写一行 send_at_cmd("AT+XXX", "期望应答", 超时) 就能接上——这是扩展整套驱动的支点。

3. 帧边界:RXNE + IDLE 空闲中断 ​

3.1 中断服务函数 ​

AT 应答里既有 \r\n(指令响应),又有 >(CIPSEND 提示符),透传时更是任意二进制。用结束符判帧必然失败,唯一可靠的判据是"总线静了下来"——STM32 的 USART 正好提供空闲中断(IDLE):一帧数据收完后总线保持空闲一个字符时间,硬件置 IDLE 标志。

c
// Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.c
void ATK_MW8266D_UART_IRQHandler(void)
{
    uint8_t tmp;

    if (__HAL_UART_GET_FLAG(&g_uart_handle, UART_FLAG_ORE) != RESET)     /* 溢出错误 */
    {
        __HAL_UART_CLEAR_OREFLAG(&g_uart_handle);                        /* 清 ORE */
        (void)g_uart_handle.Instance->SR;                                /* 顺序不能反: 先 SR */
        (void)g_uart_handle.Instance->DR;                                /* 再 DR */
    }

    if (__HAL_UART_GET_FLAG(&g_uart_handle, UART_FLAG_RXNE) != RESET)    /* 收到 1 字节 */
    {
        HAL_UART_Receive(&g_uart_handle, &tmp, 1, HAL_MAX_DELAY);        /* RXNE 已置位, 实际不阻塞 */
        if (g_uart_rx_frame.sta.len < (ATK_MW8266D_UART_RX_BUF_SIZE - 1))
        {
            g_uart_rx_frame.buf[g_uart_rx_frame.sta.len++] = tmp;
        }
        else                                                             /* 缓冲满: 丢旧留新 */
        {
            g_uart_rx_frame.sta.len = 0;
            g_uart_rx_frame.buf[g_uart_rx_frame.sta.len++] = tmp;
        }
    }

    if (__HAL_UART_GET_FLAG(&g_uart_handle, UART_FLAG_IDLE) != RESET)    /* 总线空闲 = 一帧结束 */
    {
        g_uart_rx_frame.sta.finsh = 1;
        __HAL_UART_CLEAR_IDLEFLAG(&g_uart_handle);
    }
}

3.2 三个分支的处理要点 ​

分支要点不这么做的后果
ORE先清 ORE,再读 SR、DR中断反复重入,串口彻底卡死
RXNE上限取 SIZE - 1,留一位放 '\0'get_frame() 写结束符时越界,踩相邻变量
IDLE只置 finsh,不清 len长度丢了,上层拿不到有效数据

还有一处顺序上的讲究:RXNE 分支必须放在 IDLE 分支之前。__HAL_UART_CLEAR_IDLEFLAG() 的实现是"先读 SR 再读 DR",如果此刻 RXNE 恰好置位,这次读 DR 会把刚到的那个字节吞掉。先处理 RXNE 把 DR 读空,再清 IDLE,就不会丢字节。

至于缓冲区满时"清零重来"而不是"丢弃新字节":AT 场景下最新一帧才是有用的(旧数据已经过期),这个取舍是对的,但代价是一帧超过缓冲深度就只剩尾部残片,见 4.2 节。

4. 帧缓冲与状态位域 ​

4.1 atk_mw8266d_uart_rx_get_frame() ​

该函数在 Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.c 中实现,声明位于 atk_mw8266d_uart.h:

c
uint8_t *atk_mw8266d_uart_rx_get_frame(void);

【函数作用】

若已收到完整一帧(IDLE 已置 finsh),在缓冲尾部补 '\0' 并返回缓冲首地址,供上层用 strstr 等字符串函数解析。

【参数含义】

无。

【返回值】

已收到一帧时返回内部缓冲指针;否则返回 NULL。

【补充说明】

配套的 atk_mw8266d_uart_rx_get_frame_len() 返回同一帧的长度。两个函数读的是同一份状态,所以 get_frame() 可以重复调用——atk_mw8266d_get_ip() 就是这么用的:先用 send_at_cmd("AT+CIFSR", "OK", 500) 等到应答,再从同一帧里 strstr 抠引号之间的 IP。

原厂实现把长度和"完成"标志塞进一个 uint16_t(缓冲为 128 时,长度上限就是 127):

c
// Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.c
struct
{
    uint16_t len   : 15;   /* 已收字节数, 0 ~ 127 */
    uint16_t finsh : 1;    /* 帧完成标志, 只有 rx_restart() 能清掉 */
} sta;

finsh 只由 IDLE 置位、只由 atk_mw8266d_uart_rx_restart() 清零,这个"单向"设计保证了同一帧可以被多次读取而不会自毁。

4.2 缓冲区的硬边界 ​

ATK_MW8266D_UART_RX_BUF_SIZE 原厂取 128(实际可用 127 字节),工程里提到了 256——最长的一条应答是 AT+GMR 的三行版本信息,约 110 字节,128 能过但没余量。三条现实约束:

(1)常规 AT 应答够用。AT+CIFSR 的完整应答约 70 字节。

(2)AT+CWLAP 扫描结果远超 256 字节,必然被截断成尾部残片,这个驱动不能用来做 AP 扫描解析。

(3)透传接收的数据同样受缓冲深度限制。服务器一次发 1 KB,MCU 只会拿到最后不到 256 字节。要传大块数据必须加大缓冲,或者改成"环形缓冲 + DMA",那已经不是本驱动的形态了。

5. 透传模式的进出 ​

进入透传是两条指令加一个提示符:

text
AT+CIPMODE=1        →  OK              (1: 透传, 0: 普通; 不保存到 Flash)
AT+CIPSEND          →  >               (收到 '>' 才算真正进入)

三条前置条件缺一不可:AT+CIPMUX=0(透传只支持单连接)、连接已建立(TCP 客户端或 UDP 固定对端)、AT+CIPMODE=1 先于 AT+CIPSEND。

驱动里对应的代码把两条指令的返回值累加判断,只有两条都 EOK 才算进入:

c
// Drivers/BSP/ATK_MW8266D/atk_mw8266d.c
ret  = atk_mw8266d_send_at_cmd("AT+CIPMODE=1", "OK", 500);
ret += atk_mw8266d_send_at_cmd("AT+CIPSEND",    ">",  500);

退出透传是三个字符、不带回车换行:

c
// Drivers/BSP/ATK_MW8266D/atk_mw8266d.c
void atk_mw8266d_exit_unvarnished(void)
{
    atk_mw8266d_uart_printf("+++");
}

【注意】

+++ 前后各需要至少 1 s 的串口静默,手册明确要求"发送 +++ 退出透传后,至少间隔 1 秒再发下一条 AT 指令"。原厂实现发完就返回、没有延时,紧接着发 AT 指令有概率被当作透传数据处理。移植时建议在调用 exit_unvarnished() 之后补一句 HAL_Delay(1000)。

还有一个特性要记住:透传模式下如果连接断开,模块会不停自动重连,此时 +++ 是唯一能停下来的手段;而普通模式下断线不会重连,模块只打印 CLOSED。

6. 返回值与错误归一 ​

头文件定义了四个错误码:

宏值含义
ATK_MW8266D_EOK0成功
ATK_MW8266D_ERROR1通用失败(含超时归一)
ATK_MW8266D_ETIMEOUT2等期望应答超时,只有 send_at_cmd() 会返回它
ATK_MW8266D_EINVAL3参数非法(如 set_mode(4)、ate_config(2))

业务层函数一律把 ETIMEOUT 折叠成 ERROR,调用者只需要判"0 / 非 0"。这个设计简单够用,代价是丢失了"超时"与"参数错"的区分,排查时只能靠串口日志。

三、接入 CubeMX 工程 ​

1. 使能 RXNE 与 IDLE 中断 ​

放在驱动的串口初始化函数里,紧接 CubeMX 的初始化之后:

c
// Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.c
void atk_mw8266d_uart_init(uint32_t baudrate)
{
    (void)baudrate;   /* 波特率由 CubeMX 的 MX_USART3_UART_Init() 设定, 此处不再 HAL_UART_Init() */

    __HAL_UART_ENABLE_IT(&huart3, UART_IT_RXNE);   /* 收到 1 字节就进中断 */
    __HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);   /* 总线空闲进中断, 用于判帧结束 */
}

【注意】

这里不要再调 HAL_UART_Init()。CubeMX 生成的 MX_USART3_UART_Init() 已经初始化过,重复初始化会把 NVIC 与引脚配置重走一遍,还会与 huart3 的内部状态机打架。

2. 把驱动中断挂到生成的向量处理函数 ​

CubeMX 在 Core/Src/stm32f1xx_it.c 里生成:

c
void USART3_IRQHandler(void)
{
  /* USER CODE BEGIN USART3_IRQn 0 */
  /* USER CODE END USART3_IRQn 0 */
  HAL_UART_IRQHandler(&huart3);
  /* USER CODE BEGIN USART3_IRQn 1 */
  /* USER CODE END USART3_IRQn 1 */
}

只需在 USER CODE BEGIN USART3_IRQn 0 里插入一行:

c
  /* USER CODE BEGIN USART3_IRQn 0 */
  atk_mw8266d_uart_irq_handler();   /* 自己处理 RXNE + IDLE + ORE */
  /* USER CODE END USART3_IRQn 0 */
  HAL_UART_IRQHandler(&huart3);

后面的 HAL_UART_IRQHandler(&huart3) 可以保留,理由是:

(1)RXNE 已被自定义代码读 DR 清掉,HAL 查不到标志位;即使查到,huart3.RxState 是 READY,UART_Receive_IT() 会直接返回 HAL_BUSY,不会二次消费。

(2)F1 的 HAL 版本里 HAL_UART_IRQHandler() 不处理 IDLE 标志(源码中只判 PE/FE/ORE/NE 与 RXNE),所以两套逻辑不会争同一个标志。

(3)出错时 HAL 仍会走到 HAL_UART_ErrorCallback(),保留了一个统一的错误观测点。

反过来说,绝对不要把原厂头文件里这一行一起搬过来:

c
#define ATK_MW8266D_UART_IRQHandler  USART3_IRQHandler   /* 原厂写法, 搬迁时必须删掉 */

它会与 CubeMX 生成的 USART3_IRQHandler 定义重复,链接期报 multiply defined;即使靠宏顺序侥幸编过,两套中断逻辑也会互相踩缓冲。

3. 重新生成时哪些内容会被保留 ​

这一节是实测结论,不是推测:拿同一份 .ioc 在 CubeMX 里重新执行一次 project generate,再逐文件比对哈希。

文件结果原因
flash-app.ioc不变是配置的唯一事实来源,本身不被生成覆盖
Core/Src/main.c逐字节一致新增代码全在 USER CODE BEGIN Includes / PV / 2 区,且 ProjectManager.KeepUserCode=TRUE
Core/Src/usart.c逐字节一致新增代码全在 USER CODE BEGIN 0 / END 0 与 BEGIN 1 / END 1 区
Core/Src/gpio.c、Core/Inc/main.h逐字节一致PA4 与 User Label 来自 .ioc,生成结果本就相同
MDK-ARM/flash-app.uvprojx逐字节一致CubeMX 6.x 的工程生成器会保留用户自建的文件分组与 include 路径
Drivers/BSP/ATK_MW8266D/原样存在该目录不在 CubeMX 的管理范围内,它只重写自己生成过的文件

由此得到三条可以依赖的约定:

(1)配置只写 .ioc。引脚、外设、NVIC、栈大小都在 CubeMX 里点,不要手改 gpio.c/stm32f1xx_hal_msp.c——手改的内容会被下次生成覆盖。

(2)C 代码只写 USER CODE 区。写在区外的任何一行都会被覆盖,而且事后很难发现是哪一次生成吃掉的。

(3)驱动源码放在 Drivers/BSP/ 下,不要放进 Core/Src/。放 Core/Src/ 虽然不会被删,但 CubeMX 也不会把它加进工程,每生成一次都要重新手动加文件。

自检方法很简单:重新生成之后直接 git status --short,输出为空就说明一个字节都没被破坏;有 diff 就逐条看,能进 diff 的只有 .ioc(你自己改的配置)和 MDK 工程里的时间戳类字段。

会失效的只有三种自己动配置的情况:

  • 改掉 PA4 的 User Label:ATK_ESP_RST_Pin 宏名随之变化,驱动编译报"未定义",属于期望中的失败,改名后同步即可;
  • 在 .ioc 里取消 USART3 global interrupt:USART3_IRQHandler 不再生成,中断收不到数据;
  • 把 USART3 从 .ioc 里删掉:huart3 与 MX_USART3_UART_Init() 一起消失,驱动整体失效。

四、移植与使用 ​

1. 文件放置 ​

按 BSP 目录约定,六个文件放到 Drivers/BSP/ATK_MW8266D/,分三层:

text
Drivers/BSP/ATK_MW8266D/
  atk_mw8266d.c        atk_mw8266d.h        业务层: 复位、AT 指令、信息查询
  atk_mw8266d_uart.c   atk_mw8266d_uart.h   接口层: USART3 单帧收发
  atk_mw8266d_test.c   atk_mw8266d_test.h   自测层: 应答与信息查询的打印

自测层只负责"跑一遍并打印",不参与产品逻辑;测试函数统一以 _test 结尾(atk_mw8266d_at_test()、atk_mw8266d_info_test()、atk_mw8266d_test()),和应用层调用点一眼能分开。打印走 USART1 的 printf,不是模块口。

这样分层的目的是让 main.c 只留调用:初始化调 atk_mw8266d_init(),自测调 atk_mw8266d_test(),业务逻辑一行都不进 main.c。

Keil 工程里把三个 .c 加进分组、把目录加进 Include Paths;CMake 工程则加进 target_sources() 与 target_include_directories()。

2. 相对原厂代码的六处改造 ​

序号原厂写法CubeMX 工程写法原因
1#include "./SYSTEM/sys/sys.h"#include "main.h" + #include "usart.h"不再有 SYSTEM 目录
2自带 UART_HandleTypeDef g_uart_handle 并 HAL_UART_Init()extern UART_HandleTypeDef huart3; 不复位外设外设由 CubeMX 初始化
3delay_ms(n)HAL_Delay(n)使用 HAL 的 1 ms 时基
4#define ..._IRQHandler USART3_IRQHandler删除宏,函数改名为 atk_mw8266d_uart_irq_handler(),在生成的向量里调用避免与 CubeMX 生成的定义冲突
5ATK_MW8266D_UART_CLK_ENABLE() 等引脚宏删除(MSP 已做)时钟与引脚由 CubeMX 生成
6中断里 HAL_UART_Receive(..., 1, HAL_MAX_DELAY)直接 tmp = (uint8_t)(huart3.Instance->DR & 0xFFU)不经过 HAL 状态机,少一层开销与耦合

第 6 项也可以保留原厂写法(RXNE 已置位时该调用立即返回),但直接读 DR 更干净,也不会与 HAL 的 RxState 产生任何关联。

3. 改造后的接口层代码 ​

c
// Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.h
#ifndef __ATK_MW8266D_UART_H
#define __ATK_MW8266D_UART_H

#include "main.h"

#define ATK_MW8266D_UART_RX_BUF_SIZE   256    /* 帧缓冲: AT+GMR 三行版本信息约 110 字节, 留余量 */
#define ATK_MW8266D_UART_TX_BUF_SIZE   64     /* vsprintf 目标缓冲, 无边界检查 */

void     atk_mw8266d_uart_init(uint32_t baudrate);
void     atk_mw8266d_uart_printf(char *fmt, ...);
void     atk_mw8266d_uart_irq_handler(void);
void     atk_mw8266d_uart_rx_restart(void);
uint8_t *atk_mw8266d_uart_rx_get_frame(void);
uint16_t atk_mw8266d_uart_rx_get_frame_len(void);

#endif
c
// Drivers/BSP/ATK_MW8266D/atk_mw8266d_uart.c
#include "atk_mw8266d_uart.h"
#include "usart.h"          /* CubeMX 生成, 提供 huart3 */
#include <stdarg.h>
#include <stdio.h>
#include <string.h>

static struct
{
    uint8_t buf[ATK_MW8266D_UART_RX_BUF_SIZE];
    struct
    {
        uint16_t len   : 15;
        uint16_t finsh : 1;
    } sta;
} g_uart_rx_frame = {0};

static uint8_t g_uart_tx_buf[ATK_MW8266D_UART_TX_BUF_SIZE];

void atk_mw8266d_uart_init(uint32_t baudrate)
{
    (void)baudrate;
    __HAL_UART_ENABLE_IT(&huart3, UART_IT_RXNE);
    __HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
}

void atk_mw8266d_uart_irq_handler(void)
{
    uint8_t tmp;

    if (__HAL_UART_GET_FLAG(&huart3, UART_FLAG_ORE) != RESET)
    {
        __HAL_UART_CLEAR_OREFLAG(&huart3);
        (void)huart3.Instance->SR;
        (void)huart3.Instance->DR;
    }

    if (__HAL_UART_GET_FLAG(&huart3, UART_FLAG_RXNE) != RESET)
    {
        tmp = (uint8_t)(huart3.Instance->DR & 0xFFU);      /* 读 DR 即清 RXNE */
        if (g_uart_rx_frame.sta.len < (ATK_MW8266D_UART_RX_BUF_SIZE - 1))
        {
            g_uart_rx_frame.buf[g_uart_rx_frame.sta.len++] = tmp;
        }
        else
        {
            g_uart_rx_frame.sta.len = 0;                    /* 溢出: 丢旧留新 */
            g_uart_rx_frame.buf[g_uart_rx_frame.sta.len++] = tmp;
        }
    }

    if (__HAL_UART_GET_FLAG(&huart3, UART_FLAG_IDLE) != RESET)
    {
        g_uart_rx_frame.sta.finsh = 1;
        __HAL_UART_CLEAR_IDLEFLAG(&huart3);                 /* 内部先读 SR 再读 DR */
    }
}

其余函数(atk_mw8266d_uart_printf()、atk_mw8266d_uart_rx_restart()、atk_mw8266d_uart_rx_get_frame()、atk_mw8266d_uart_rx_get_frame_len())逻辑与原厂一致,只把 g_uart_handle 换成 huart3。

业务层 atk_mw8266d.c 的改造更少:把 atk_mw8266d_hw_init() 里的 GPIO 初始化整段删掉(PA4 已由 CubeMX 配好),atk_mw8266d_hw_reset() 里的 delay_ms() 换成 HAL_Delay(),引脚宏换成 CubeMX 生成的 ATK_ESP_RST_GPIO_Port / ATK_ESP_RST_Pin:

c
// Drivers/BSP/ATK_MW8266D/atk_mw8266d.h
/* 引脚宏不再自己定义: 用 CubeMX 在 main.h 里按 User Label 生成的宏 */
#define ATK_MW8266D_RST(x)                              \
    do {                                                \
        HAL_GPIO_WritePin(ATK_ESP_RST_GPIO_Port,         \
                          ATK_ESP_RST_Pin,              \
                          (x) ? GPIO_PIN_SET : GPIO_PIN_RESET); \
    } while (0)

atk_mw8266d_hw_reset() 的时序保持不变(拉低 100 ms、拉高后等 500 ms),只把 delay_ms() 换成 HAL_Delay()。

4. API 一览 ​

函数作用返回值备注
atk_mw8266d_init(baud)硬复位 + 串口中断使能 + AT 测试EOK / ERROR内部 AT 最多重试 10 次
atk_mw8266d_restore()AT+RESTOREEOK / ERROR等 ready,超时 3 s;会重启模块
atk_mw8266d_at_test()AT → OKEOK / ERROR10 次 × 500 ms
atk_mw8266d_set_mode(mode)AT+CWMODEEOK / ERROR / EINVAL1=STA,2=AP,3=STA+AP
atk_mw8266d_sw_reset()AT+RSTEOK / ERROR成功后自带 1 s 延时
atk_mw8266d_ate_config(cfg)ATE0 / ATE1EOK / ERROR / EINVAL用 get_ip() 前必须 ATE0
atk_mw8266d_join_ap(ssid, pwd)AT+CWJAPEOK / ERROR等 WIFI GOT IP,超时 10 s
atk_mw8266d_get_ip(buf)AT+CIFSREOK / ERRORbuf 至少 16 字节
atk_mw8266d_connect_tcp_server(ip, port)AT+CIPSTART="TCP",...EOK / ERROR等 CONNECT,超时 5 s;port 是字符串
atk_mw8266d_enter_unvarnished()CIPMODE=1 + CIPSENDEOK / ERROR需先建连接且 CIPMUX=0
atk_mw8266d_exit_unvarnished()发 +++无之后建议延时 1 s
atk_mw8266d_connect_atkcld(id, pwd)AT+ATKCLDSTAEOK / ERROR原子云,等 CLOUD CONNECTED,10 s
atk_mw8266d_disconnect_atkcld()AT+ATKCLDCLSEOK / ERROR
atk_mw8266d_uart_printf(fmt, ...)格式化后写串口无阻塞发送;透传时即"发数据"
atk_mw8266d_uart_rx_get_frame()取一帧缓冲指针 / NULL取完需 rx_restart()
atk_mw8266d_uart_rx_restart()清帧标志与长度无每次 send_at_cmd 前内部已调用

自测层(atk_mw8266d_test.c)三个函数,统一以 _test 结尾:

函数作用返回值备注
atk_mw8266d_at_test()打印分隔头并做 AT 探测EOK / ERROR失败时一并打印 UART 接收错误计数
atk_mw8266d_info_test()打印版本、工作模式、本机 IPEOK / ERROR三项独立判断,单项失败不影响其余两项
atk_mw8266d_test()先 at_test() 再 info_test()EOK / ERROR模块不应答时直接返回,不再查询
atk_mw8266d_at_cmd_test(arg)at <指令> 控制台命令的处理函数无指令原样发给模块,不自动补 AT+ 前缀

配合 usart1_cmd_register_arg("at", atk_mw8266d_at_cmd_test),串口上就能用 at AT+GMR、at AT+CIFSR、at AT+CWMODE=1 这类子命令直接操作模块,见到什么应答就打印什么,不需要每次改代码重新烧录。

命令系统为此扩展了两处:usart1_cmd_register_arg() 只比较命令名(其后必须是行尾或空白),其余内容作为参数传入,参数在中断里就复制一份快照,避免被下一行覆盖;单行上限从 16 字节提到 64 字节,at AT+CWJAP="ssid","pwd" 这类长指令才装得下。

上面两张表里,前一张是原厂驱动的全量接口,当前工程只实现了其中与"初始化 + 应答 + 信息查询"相关的那部分(init、ping、send_cmd、set_echo、get_version、get_mode、get_ip),联网、TCP、透传均未移植。

5. 最小调用序列 ​

接到 CubeMX 工程后,main() 里就这几步:

c
// Core/Src/main.c
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_USART3_UART_Init();

if (atk_mw8266d_init(115200) != ATK_MW8266D_EOK)
{
    printf("ATK-MW8266D init failed\r\n");      /* 十有八九是接线/P8 跳线/PA4 电平 */
    Error_Handler();
}

if (atk_mw8266d_ate_config(0) != ATK_MW8266D_EOK) { Error_Handler(); }   /* 关回显, get_ip 依赖它 */
if (atk_mw8266d_set_mode(1)  != ATK_MW8266D_EOK) { Error_Handler(); }   /* STA */
if (atk_mw8266d_join_ap("SSID", "PASSWORD") != ATK_MW8266D_EOK) { Error_Handler(); }

atk_mw8266d_get_ip(ip_buf);                                            /* 失败也先打印看看 */
printf("IP: %s\r\n", ip_buf);

if (atk_mw8266d_connect_tcp_server("192.168.1.3", "8080") != ATK_MW8266D_EOK)
{
    printf("connect tcp server failed\r\n");
    Error_Handler();
}

atk_mw8266d_enter_unvarnished();      /* 之后串口即数据通道 */
atk_mw8266d_uart_rx_restart();

while (1)
{
    uint8_t *frame = atk_mw8266d_uart_rx_get_frame();
    if (frame != NULL)
    {
        printf("RX: %s\r\n", frame);
        atk_mw8266d_uart_rx_restart();
    }
    atk_mw8266d_uart_printf("hello from stm32\r\n");
    HAL_Delay(1000);
}

注意 atk_mw8266d_init() 要在 MX_USART3_UART_Init() 之后调用,因为它只负责使能中断位,前提是外设已经初始化。

6. 可配置宏 ​

宏位置默认说明
ATK_MW8266D_UART_RX_BUF_SIZEatk_mw8266d_uart.h256(原厂 128)帧缓冲,加大可缓解长应答截断,但会吃 RAM
ATK_MW8266D_UART_TX_BUF_SIZEatk_mw8266d_uart.h64vsprintf 目标缓冲,无边界检查
DEMO_WIFI_SSID / DEMO_WIFI_PWD应用层—必须是 2.4 GHz 热点
DEMO_TCP_SERVER_IP / DEMO_TCP_SERVER_PORT应用层—端口以字符串传入

ATK_MW8266D_UART_TX_BUF_SIZE 是 64,而一条 AT+CWJAP="最长 32 字节 SSID","最长 63 字节密码" 拼起来会远超 64,vsprintf 直接越界写。实际 SSID + 密码总长建议控制在 40 字符以内;更长就把缓冲加到 128 以上。

驱动装好、能编过之后,剩下的工作是验证:环境怎么搭、日志该长什么样、出错先看哪几个状态量。这部分单独成篇,见 LV030-ATK-MW8266D联调与排错。