LV025-ATK-MW8266D驱动实现
模块的接线与 CubeMX 外设配置在LV020-ATK-MW8266D硬件连接与工程配置里已经完成,这一篇接着往下走:AT 指令怎么收发、一帧应答怎么判定结束、驱动怎么从原厂工程搬进 CubeMX 工程,以及移植时要改哪几处。
一、参考实例的程序结构
1. 文件组成
参考工程是"ATK-ESP8266 模块 TCP 透传实验 / 战舰 STM32F103"。与本模块相关的只有下面几个文件,其余是 LED、KEY、LCD、SYSTEM 等公共驱动:
| 文件 | 职责 |
|---|---|
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.c | USART1 调试口,以及 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 闪烁的死循环:
// 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 |
| 服务器 → MCU | atk_mw8266d_uart_rx_get_frame() + atk_mw8266d_uart_rx_restart() | 按空闲判帧取走一段,取完必须 restart |
透传期间不能再发 AT 指令(会被当成数据发给服务器),唯一的出口是先发 +++ 退出。
二、驱动核心机制
1. 原厂的三个骨架函数
三个函数串起来就是整个驱动的模型:init() 负责把模块叫醒,at_test() 负责确认链路通,send_at_cmd() 负责一次问答。
1.1 atk_mw8266d_init()
/**
* @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()
/**
* @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:
uint8_t atk_mw8266d_send_at_cmd(char *cmd, char *ack, uint32_t timeout);【函数作用】
向模块发送一条 AT 指令,并在给定时间内轮询接收帧,判断应答中是否包含期望字符串。驱动里所有业务函数(连 AP、连服务器、进透传……)都建立在它之上。
【参数含义】
cmd:不含\r\n的指令正文,函数内部自动补\r\nack:期望在应答中出现的子串,如"OK"、"WIFI GOT IP"、">";传NULL表示只发不收timeout:等待时间,单位 ms(实现上是 1 ms 递减的轮询)
【返回值】
成功返回 ATK_MW8266D_EOK(0);等待期内没等到期望应答返回 ATK_MW8266D_ETIMEOUT(2)。
原厂完整实现:
/**
* @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 标志。
// 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:
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):
// 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. 透传模式的进出
进入透传是两条指令加一个提示符:
AT+CIPMODE=1 → OK (1: 透传, 0: 普通; 不保存到 Flash)
AT+CIPSEND → > (收到 '>' 才算真正进入)三条前置条件缺一不可:AT+CIPMUX=0(透传只支持单连接)、连接已建立(TCP 客户端或 UDP 固定对端)、AT+CIPMODE=1 先于 AT+CIPSEND。
驱动里对应的代码把两条指令的返回值累加判断,只有两条都 EOK 才算进入:
// 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);退出透传是三个字符、不带回车换行:
// 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_EOK | 0 | 成功 |
ATK_MW8266D_ERROR | 1 | 通用失败(含超时归一) |
ATK_MW8266D_ETIMEOUT | 2 | 等期望应答超时,只有 send_at_cmd() 会返回它 |
ATK_MW8266D_EINVAL | 3 | 参数非法(如 set_mode(4)、ate_config(2)) |
业务层函数一律把 ETIMEOUT 折叠成 ERROR,调用者只需要判"0 / 非 0"。这个设计简单够用,代价是丢失了"超时"与"参数错"的区分,排查时只能靠串口日志。
三、接入 CubeMX 工程
1. 使能 RXNE 与 IDLE 中断
放在驱动的串口初始化函数里,紧接 CubeMX 的初始化之后:
// 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 里生成:
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 里插入一行:
/* 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(),保留了一个统一的错误观测点。
反过来说,绝对不要把原厂头文件里这一行一起搬过来:
#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/,分三层:
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 初始化 |
| 3 | delay_ms(n) | HAL_Delay(n) | 使用 HAL 的 1 ms 时基 |
| 4 | #define ..._IRQHandler USART3_IRQHandler | 删除宏,函数改名为 atk_mw8266d_uart_irq_handler(),在生成的向量里调用 | 避免与 CubeMX 生成的定义冲突 |
| 5 | ATK_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. 改造后的接口层代码
// 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// 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:
// 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+RESTORE | EOK / ERROR | 等 ready,超时 3 s;会重启模块 |
atk_mw8266d_at_test() | AT → OK | EOK / ERROR | 10 次 × 500 ms |
atk_mw8266d_set_mode(mode) | AT+CWMODE | EOK / ERROR / EINVAL | 1=STA,2=AP,3=STA+AP |
atk_mw8266d_sw_reset() | AT+RST | EOK / ERROR | 成功后自带 1 s 延时 |
atk_mw8266d_ate_config(cfg) | ATE0 / ATE1 | EOK / ERROR / EINVAL | 用 get_ip() 前必须 ATE0 |
atk_mw8266d_join_ap(ssid, pwd) | AT+CWJAP | EOK / ERROR | 等 WIFI GOT IP,超时 10 s |
atk_mw8266d_get_ip(buf) | AT+CIFSR | EOK / ERROR | buf 至少 16 字节 |
atk_mw8266d_connect_tcp_server(ip, port) | AT+CIPSTART="TCP",... | EOK / ERROR | 等 CONNECT,超时 5 s;port 是字符串 |
atk_mw8266d_enter_unvarnished() | CIPMODE=1 + CIPSEND | EOK / ERROR | 需先建连接且 CIPMUX=0 |
atk_mw8266d_exit_unvarnished() | 发 +++ | 无 | 之后建议延时 1 s |
atk_mw8266d_connect_atkcld(id, pwd) | AT+ATKCLDSTA | EOK / ERROR | 原子云,等 CLOUD CONNECTED,10 s |
atk_mw8266d_disconnect_atkcld() | AT+ATKCLDCLS | EOK / 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() | 打印版本、工作模式、本机 IP | EOK / 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() 里就这几步:
// 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_SIZE | atk_mw8266d_uart.h | 256(原厂 128) | 帧缓冲,加大可缓解长应答截断,但会吃 RAM |
ATK_MW8266D_UART_TX_BUF_SIZE | atk_mw8266d_uart.h | 64 | vsprintf 目标缓冲,无边界检查 |
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联调与排错。