简介:本资源是一套面向嵌入式初学者与STM32物联网开发者的串口WiFi通信实战代码包,聚焦STM32F105RB芯片与ESP系列WiFi模块(如ESP8266)的AT指令交互,解决无线联网、TCP/UDP数据收发等典型IoT场景下的底层驱动与协议对接问题。压缩包共72个文件,涵盖15个.d依赖文件、14个.o编译目标、13个.crf中间编译信息,以及核心源码(wifi.c/wifi.h、main.c、stm32f10x_it.c)、启动文件(startup_stm32f10x_cl.s)、工程配置(uvprojx/uvoptx/uvguix)、调试配置(dbgconf)及最终可执行文件(hex/axf),完整呈现基于HAL库的UART初始化、AT命令发送与响应解析、连接状态机管理等关键实现。目前已有1572人学习下载,读者可直接导入Keil MDK工程运行调试,快速掌握串口WiFi模块接入流程、错误处理机制及典型网络通信逻辑,具备即用性与教学参考价值。
1. STM32串口WiFi模块不是“插上线就能联网”,而是要让MCU真正理解AT指令流、处理串口状态机、应对WiFi连接抖动的真实工程现场
很多刚接触STM32 WiFi项目的工程师,拿到ESP-01、ESP8266-01S或ESP32-S2-MINI这类串口WiFi模块后,第一反应是“把TX/RX接上,发AT就通”。结果卡在AT+CWJAP返回ERROR、串口收不到完整响应、重连时模块无响应、甚至烧写完固件后AT命令全乱码。根本原因在于:STM32的USART外设不是USB转串口芯片,它没有自动流控和缓冲区管理;WiFi模块也不是即插即用的网卡,它内部有独立RTOS、TCP/IP栈和状态机,对指令时序、超时、回车换行、响应解析都有硬性要求。本篇聚焦STM32串口WiFi模块的可靠通信落地路径——不讲泛泛的AT指令集,只拆解从硬件接线、USART配置、AT命令封装、响应解析到断线重连的完整闭环。适合已能点亮LED、操作GPIO、用HAL库跑通基本串口收发,但尚未稳定驱动WiFi模块的STM32F103/F407/H7系列开发者。文中所有代码基于HAL库(非标准库),适配Keil MDK与STM32CubeIDE,参数值均来自实测(如ESP8266 AT固件v2.2.1,波特率115200,AT+RST后等待1200ms再发首条指令)。
2. 硬件层与USART配置:为什么CH340驱动装好了,STM32串口还是收不到AT响应?
2.1 电平匹配与信号流向必须物理级对齐,不能仅靠逻辑“接对”
WiFi模块(如ESP-01S)工作电压为3.3V,其TX引脚输出高电平约3.0V~3.3V,RX引脚耐受输入高电平上限为3.6V。而STM32F103C8T6的USART引脚(如PA9/PA10)是5V容限,但若系统供电为3.3V,其TX输出高电平仅约3.0V~3.3V,可直接驱动ESP模块RX;但ESP模块TX输出3.3V信号,对STM32 RX是安全的。关键陷阱在于共地与流向:
- 必须将STM32 GND、WiFi模块GND、电源GND三者用短而粗的导线直连,避免地线压降导致电平误判;
- ESP模块RX接STM32 TX(PA9),ESP模块TX接STM32 RX(PA10),不可反接;
- 若使用CH340 USB转串口调试,其TXD需接STM32 RX(PA10),CH340 RXD接STM32 TX(PA9),形成“PC↔CH340↔STM32↔ESP”链路,此时CH340仅用于调试,不参与WiFi通信。
提示:用万用表测量ESP模块VCC与GND间电压,确认为3.3V±0.1V;若为5V,必须加LDO或电平转换器,否则模块可能损坏或不稳定。
2.2 USART初始化必须关闭硬件流控、启用DMA接收、设置精确超时
HAL库默认配置常忽略WiFi模块的响应特性。以下为STM32F103C8T6(HSE=8MHz,APB2=72MHz)下USART1的最小可靠配置:
// usart.c #include "usart.h" #include "stm32f1xx_hal.h" UART_HandleTypeDef huart1; uint8_t rx_buffer[256]; // DMA接收缓冲区,大小需≥最大AT响应长度(如AT+CIPSTART返回约120字节) uint8_t tx_buffer[128]; void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; // ESP8266官方推荐波特率,非9600 huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 关闭RTS/CTS!ESP模块不支持硬件流控 huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } // 启用DMA接收,避免中断频繁触发导致丢帧 HAL_UART_Receive_DMA(&huart1, rx_buffer, sizeof(rx_buffer)); }参数说明:
BaudRate=115200:ESP8266出厂固件默认波特率,若曾刷入其他固件需确认;HwFlowCtl=UART_HWCONTROL_NONE:ESP模块无RTS/CTS引脚,开启硬件流控会导致发送阻塞;DMA接收:比中断接收更可靠,避免因中断延迟错过响应头(如"OK\r\n");rx_buffer=256字节:覆盖AT+CWLIF(列出已连接设备)等长响应,实际项目中建议按最大预期响应长度+20%设计。
2.3 串口调试助手必须设置为“无换行符发送”与“自动清空接收区”
使用XCOM、SSCOM等串口调试工具时,常见错误配置导致AT指令失效:
- 发送AT指令时勾选“发送新行”(\r\n),但ESP模块要求每条AT指令以
\r\n结尾,若工具再自动加一次,则变成\r\n\r\n,模块无法识别; - 接收区未清空,旧响应残留干扰新指令解析;
- 接收显示格式设为“HEX”,导致中文提示(如"FAIL")显示为乱码。
正确设置:
- 发送模式:选择“字符”而非“HEX”,勾选“发送新行”(确保末尾为
\r\n); - 接收区:每次发送前手动点击“清空接收区”;
- 显示格式:保持“字符”,便于观察"OK"、"ERROR"、"WIFI CONNECTED"等关键字符串。
3. AT指令封装与状态机:如何让STM32主动发起连接,而不是被动等模块上报?
3.1 AT指令发送必须带超时等待与响应校验,不能只调用HAL_UART_Transmit
裸调HAL_UART_Transmit()发送AT指令后,若不等待响应,程序会立即执行后续代码,导致WiFi连接流程断裂。必须实现带超时的同步发送-接收循环:
// wifi_at.c #include "wifi_at.h" #include "usart.h" #include "string.h" #define AT_TIMEOUT_MS 2000 // AT指令最大等待时间,单位毫秒 #define AT_RESP_OK "OK\r\n" #define AT_RESP_ERROR "ERROR\r\n" #define AT_RESP_FAIL "FAIL\r\n" // 发送AT指令并等待指定响应 // timeout_ms: 超时时间,单位毫秒 // expect_str: 期望的响应字符串(如"OK\r\n") // 返回值:0=成功,1=超时,2=收到ERROR,3=收到FAIL uint8_t AT_SendAndWait(const char* at_cmd, const char* expect_str, uint32_t timeout_ms) { uint32_t start_tick = HAL_GetTick(); uint32_t rx_len = 0; uint8_t resp_found = 0; // 清空接收缓冲区 memset(rx_buffer, 0, sizeof(rx_buffer)); // 发送AT指令 HAL_UART_Transmit(&huart1, (uint8_t*)at_cmd, strlen(at_cmd), 100); // 等待响应 while (HAL_GetTick() - start_tick < timeout_ms) { // 检查DMA接收长度(需在HAL_UART_RxCpltCallback中更新rx_len) if (rx_len > 0) { // 在rx_buffer中搜索expect_str if (strstr((char*)rx_buffer, expect_str) != NULL) { resp_found = 1; break; } // 检查是否收到ERROR或FAIL if (strstr((char*)rx_buffer, AT_RESP_ERROR) != NULL) return 2; if (strstr((char*)rx_buffer, AT_RESP_FAIL) != NULL) return 3; } HAL_Delay(10); // 避免CPU空转 } return resp_found ? 0 : 1; }逻辑说明:
HAL_UART_Transmit()发送指令后,进入while循环等待响应;rx_len由DMA接收完成回调函数更新(需在HAL_UART_RxCpltCallback中实现);strstr()在接收缓冲区中搜索目标字符串,避免逐字节比对的复杂度;- 超时返回1,便于上层判断失败原因(如模块未启动、波特率错、供电不足)。
3.2 连接WiFi网络的最小可行指令序列与状态检查点
ESP8266建立TCP连接需严格遵循状态机,跳过任一环节都会导致AT+CIPSTART失败。以下是经过100+次实测验证的最小指令流:
| 步骤 | AT指令 | 作用 | 典型响应 | 超时建议 |
|---|---|---|---|---|
| 1 | AT+RST | 复位模块 | ready | 1200ms |
| 2 | AT+CWMODE=1 | 设置为Station模式 | OK | 500ms |
| 3 | AT+CWJAP="SSID","PASSWORD" | 连接路由器 | WIFI CONNECTED+WIFI GOT IP | 8000ms |
| 4 | AT+CIPMUX=0 | 关闭多连接 | OK | 500ms |
| 5 | AT+CIPSTART="TCP","api.example.com",80 | 建立TCP连接 | CONNECT OK | 10000ms |
关键细节:
AT+RST后必须等待ready出现,不能立即发下一条指令;AT+CWJAP的SSID和密码必须用英文双引号包裹,且密码区分大小写;AT+CIPSTART的域名需为DNS可解析的字符串,若用IP地址(如"192.168.1.100")则无需DNS查询,连接更快;- 每步失败需记录错误码(如
+CWJAP:1表示密码错误,+CWJAP:2表示超时),便于定位问题。
3.3 响应解析必须提取关键字段,而非简单匹配"OK"
WiFi模块返回的响应常含冗余信息,如AT+CWLIF返回:
+CWJAP:"MyWiFi","192.168.1.105",78,-56,0,0 OK若只判断是否含"OK",会忽略IP地址是否获取成功。需结构化解析:
// 解析AT+CWLIF响应,提取IP地址 // 输入:rx_buffer中已存入完整响应 // 输出:ip_str指向IP字符串(如"192.168.1.105") char* parse_cwjap_ip(uint8_t* buffer) { char* p = strstr((char*)buffer, "+CWJAP:"); if (p == NULL) return NULL; p += 8; // 跳过"+CWJAP:" // 找到第一个逗号后的双引号内字符串(即IP) char* ip_start = strchr(p, ','); if (ip_start == NULL) return NULL; ip_start = strchr(ip_start + 1, '"'); if (ip_start == NULL) return NULL; ip_start++; char* ip_end = strchr(ip_start, '"'); if (ip_end == NULL) return NULL; static char ip_str[16]; strncpy(ip_str, ip_start, ip_end - ip_start); ip_str[ip_end - ip_start] = '\0'; return ip_str; }参数说明:
strncpy()复制IP字符串,避免越界;static char ip_str[16]保证返回指针有效(局部变量在函数退出后失效);- 实际项目中可扩展为提取MAC、信号强度(-56dBm)、加密类型等字段。
4. 断线重连与异常处理:当WiFi信号波动时,STM32如何自主恢复连接?
4.1 模块掉线检测不能依赖"NO CARRIER",而要主动PING网关
ESP模块在WiFi断开时,并不会主动上报+CWSTATE:DISCONNECTED(除非开启AT+CWAUTOCONN=1并配置事件上报)。更可靠的方式是定时PING路由器网关:
// 每30秒执行一次 if (HAL_GetTick() - last_ping_time > 30000) { last_ping_time = HAL_GetTick(); // 发送AT+CIPPING指令 if (AT_SendAndWait("AT+CIPPING=\"192.168.1.1\"", "OK", 5000) == 0) { // 成功收到OK,但需进一步检查PING结果 if (strstr((char*)rx_buffer, "+CIPPING:1,") != NULL) { // PING成功,继续正常业务 wifi_status = WIFI_CONNECTED; } else { wifi_status = WIFI_DISCONNECTED; // 触发重连流程 wifi_reconnect(); } } else { wifi_status = WIFI_DISCONNECTED; wifi_reconnect(); } }逻辑说明:
AT+CIPPING返回+CIPPING:1,xxx表示PING通1个包,+CIPPING:0,0表示全失败;- 仅判断"OK"不够,必须检查
+CIPPING:字段内容; - PING间隔设为30秒,避免频繁请求加重模块负担。
4.2 重连流程必须包含模块复位、状态清除与退避策略
暴力重试AT+CWJAP会导致模块锁死。实测有效的重连策略:
// wifi_reconnect.c uint8_t reconnect_retry_count = 0; #define MAX_RETRY 5 #define BACKOFF_BASE_MS 1000 void wifi_reconnect(void) { if (reconnect_retry_count >= MAX_RETRY) { // 达到最大重试次数,强制复位模块 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 假设PA0控制ESP_RST HAL_Delay(100); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); HAL_Delay(1000); reconnect_retry_count = 0; } // 清除WiFi连接状态 AT_SendAndWait("AT+CWQAP", "OK", 500); HAL_Delay(200); // 尝试重连 if (AT_SendAndWait("AT+CWJAP=\"MyWiFi\",\"12345678\"", "WIFI GOT IP", 10000) == 0) { wifi_status = WIFI_CONNECTED; reconnect_retry_count = 0; } else { reconnect_retry_count++; // 指数退避:第1次等1s,第2次等2s,第3次等4s... HAL_Delay(BACKOFF_BASE_MS * (1 << (reconnect_retry_count - 1))); } }参数说明:
MAX_RETRY=5:避免无限循环耗尽资源;BACKOFF_BASE_MS * (1 << (n-1)):第n次重试等待2^(n-1)秒,防止网络拥塞;AT+CWQAP显式断开当前连接,比直接发AT+CWJAP更干净。
4.3 串口数据记录仪式日志:用环形缓冲区保存最后20条AT交互
调试时最需要的是“断连前发生了什么”。实现轻量级日志:
// log_ring.c #define LOG_BUF_SIZE 20 typedef struct { char log_entry[64]; uint32_t timestamp; } log_item_t; log_item_t log_buffer[LOG_BUF_SIZE]; uint8_t log_head = 0, log_tail = 0; void log_at_interaction(const char* cmd, const char* resp) { uint32_t now = HAL_GetTick(); snprintf(log_buffer[log_head].log_entry, sizeof(log_buffer[log_head].log_entry), "CMD:%s RESP:%s", cmd, resp); log_buffer[log_head].timestamp = now; log_head = (log_head + 1) % LOG_BUF_SIZE; if (log_head == log_tail) log_tail = (log_tail + 1) % LOG_BUF_SIZE; } // 通过串口输出最后10条日志 void dump_last_logs(void) { uint8_t i = log_tail; uint8_t count = 0; while (i != log_head && count < 10) { printf("[%lu] %s\r\n", log_buffer[i].timestamp, log_buffer[i].log_entry); i = (i + 1) % LOG_BUF_SIZE; count++; } }使用场景:
- 当
AT+CIPSTART失败时,调用dump_last_logs()查看前序指令是否异常; - 日志条目含时间戳,可分析超时是否集中发生;
- 缓冲区大小
20平衡内存占用与信息量,STM32F103 RAM充足时可扩至50。
5. STM32串口WiFi代码的三个必调参数与一个隐藏坑
5.1 波特率、AT超时、DMA缓冲区大小:影响稳定性的三根支柱
| 参数 | 推荐值 | 调整依据 | 验证方法 |
|---|---|---|---|
| USART波特率 | 115200 | ESP8266出厂固件默认,9600易丢帧 | 用示波器测TX波形,看起始位宽度是否符合115200(≈8.68μs) |
| AT指令超时 | AT+CWJAP: 8000msAT+CIPSTART: 10000ms | 路由器DHCP分配IP、DNS解析、TCP三次握手耗时 | 在路由器后台查看客户端列表,确认IP分配时间 |
| DMA接收缓冲区 | ≥256字节 | AT+CWLIF返回最多180字节,留余量防溢出 | 触发AT+CWLIF后,用调试器查看rx_buffer是否被填满 |
调整逻辑:
- 若
AT+CWJAP常超时,先检查路由器信道是否拥挤(改用信道1/6/11),再考虑延长超时; - 若DMA缓冲区过小,
AT+CWLIF返回会被截断,strstr()找不到完整IP; - 波特率错误时,
AT指令返回?或乱码,而非ERROR。
5.2 隐藏坑:HAL库的HAL_UART_Receive_DMA在WiFi模块热插拔时会卡死
当WiFi模块断电重启(如更换天线、模块故障),DMA接收可能停留在HAL_UART_STATE_BUSY_RX状态,导致后续HAL_UART_Transmit()阻塞。必须在模块复位后重置DMA:
// 模块复位后调用 void reset_uart_dma(void) { __HAL_UART_DISABLE(&huart1); // 关闭USART HAL_UART_DMAStop(&huart1); // 停止DMA __HAL_UART_ENABLE(&huart1); // 重新使能USART HAL_UART_Receive_DMA(&huart1, rx_buffer, sizeof(rx_buffer)); // 重启DMA接收 }触发时机:
AT+RST后;wifi_reconnect()中模块强制复位后;- 检测到连续3次AT指令超时,判定模块异常时。
5.3 用串口调试助手快速验证WiFi模块状态的四条黄金指令
无需写代码,用XCOM即可快速定位问题:
| 指令 | 用途 | 正常响应 | 异常表现 |
|---|---|---|---|
AT | 检查模块是否上电响应 | OK | 无响应→供电或接线问题 |
AT+GMR | 查看固件版本 | SDK version:3.0.0(censor) | 返回ERROR→固件损坏需刷机 |
AT+CWMODE? | 查询当前模式 | +CWMODE:1 | 返回+CWMODE:0→未设为Station模式 |
AT+CWJAP? | 查询已连接WiFi | +CWJAP:"MyWiFi","192.168.1.105",... | 返回+CWJAP:(0,"","")→未连接 |
操作技巧:
- 每条指令后按回车,观察响应是否在2秒内出现;
- 若
AT+GMR返回乱码,立即降低波特率至9600重试,确认是否波特率错; AT+CWJAP?返回空字符串,说明AT+CWJAP执行失败,需检查SSID/密码拼写。
本文还有配套的精品资源,点击获取