简介:针对STM32F407微控制器与WiFi模块通过UART实现无线通信的实验资源包,基于百科荣创嵌入式开发平台,面向正在学习STM32串口编程与无线数传的开发者。压缩包内共48个文件,含C源码、头文件、Keil工程设置(.uvproj/.uvopt)、编译生成的hex/axf固件、map/lst调试输出及备份文件,包体仅652KB,下载后可完整打开工程核对配置。已有235人学习。资源围绕UART链路展开,从GPIO与USART参数配置、HAL库收发函数调用,到利用AT指令控制ESP8266/ESP32模块入网,给出完整代码与编译结果,可作为STM32F407串口实验的参照模板。通过阅读源码与工程文件,也能排查串口不稳定、WiFi连接失败等常见问题,适合智能车及嵌入式无线控制场景的快速实践。
1. 先看懂 WiFi-UART 实验:数据是怎么从手机走到车轮的
手机上的按键点一下,车上的 STM32F407 在几毫秒内就把 PWM 占空比改了,车轮跟着转。整个过程里,WiFi 模块只做一件安静的事:把收到的网络报文原样转成 UART 字节流,再交给单片机。这个看起来简单的链路,恰恰是 WiFi-UART 实验里最容易被低估的部分——多数人卡在手机连不上模块、串口收到乱码,或者数据明明到了但车就是不动。这篇笔记以 STM32F407 加常见 WiFi 透传模块为例,把这类实验包的接线、AT 指令配置、串口帧解析和电机 PWM 映射拆开讲。适合正在做课程设计或智能车项目、手里有一份类似实验例程的同学,也适合想自己拼一台 WiFi 遥控车的实践者。
2. 硬件链路与接线:数据从手机到车轮的三个关键节点
2.1 先看整体框架:WiFi-UART 数据流和模块选型
这类实验的核心不是「WiFi」,也不是「UART」,而是「透传」。WiFi 模块在系统里扮演一个透明管道:手机 App 的 TCP 报文到达模块,模块把负载剥出来,从串口 TX 脚按字节吐给 STM32;反过来,STM32 从串口发出去的字节,模块打包成 TCP 报文发给手机。管道的两端各自只关心自己那一侧的协议,谁也不需要理解对方。
数据流向是这样的:手机 App → 路由器 → WiFi 模块(射频接收)→ 模块串口 TX → F407 的 USART2_RX → 单片机解析帧 → GPIO 控制方向、PWM 控制速度 → 电机驱动板 → 电机。任何一个环节断了,现象都差不多:车不动。这也是为什么排查时必须按链路一段一段验证,而不是盯着代码看半天。
模块选型上,绝大多数实验包和课程设计用的是 ESP8266 这类带 AT 固件的串口 WiFi 模块,理由是 F407 的 USART 资源多、处理能力足够,UART 透传对固件和上位机都最友好。你不需要了解 802.11 协议细节,只需要把模块当成一个「带 WiFi 的串口外设」来配置。模块工作状态只有两种:命令模式(AT 指令)和透传模式(数据直通),实验里的状态切换全部由 AT 指令完成。
2.2 F407 与 WiFi 模块的引脚连接和电源建议
接线是整个实验第一个翻车点。模块的 TXD 要接 F407 的 RX,模块的 RXD 要接 F407 的 TX,交叉连接,不是直连。很多同学第一次接反,发 AT 指令石沉大海。F407 上我一般用 USART2 接模块,把 USART1 留给板载 USB 转串口做调试输出,这样一边跑业务一边看日志,不用来回拔线。具体引脚以你的板子原理图为准,别照抄别人的板子。
| WiFi 模块引脚 | F407 引脚 | 说明 |
|---|---|---|
| VCC | 3.3V(建议独立供电) | 模块电源,见下方供电说明 |
| GND | GND | 必须与 F407 共地 |
| TXD | PA3(USART2_RX) | 模块发送数据给 F407 |
| RXD | PA2(USART2_TX) | F407 发送数据给模块 |
| EN / CH_PD | 3.3V | 使能引脚,悬空可能导致模块不工作 |
供电是这里最隐蔽的坑。ESP8266 这类模块在射频发射瞬间电流峰值能到 300mA 以上,如果直接从 F407 板载 3.3V 引脚取电,电压会被拉低,轻则模块随机重启,重则 F407 也跟着复位。我一般会单独用一路 3.3V 给模块供电,比如从 5V 经过 AMS1117-3.3 稳压后再供给模块,或者用独立的 3.3V 电源模块,GND 和 F407 共地即可。
注意:不要用 F407 的 3.3V 引脚直接给 ESP8266 供电,这是「一上电整车复位」最常见的元凶。
2.3 上电前用串口助手裸测 UART 链路
接 F407 之前,先把模块单独用 USB-TTL 线接电脑,排除模块自身问题。步骤如下:USB-TTL 的 TXD 接模块 RXD,RXD 接模块 TXD,VCC 接 3.3V,GND 共地;打开串口助手,波特率先试 115200,发AT\r\n,正常会回OK。如果没反应,逐档换 9600、57600、74880 试,有些旧固件默认 9600,有些模块上电会在 74880 打印乱码引导信息,那是正常现象。
模块确认没问题后,再用 USB-TTL 接 F407 的调试串口,确认单片机串口能收发。最后才把模块和 F407 接起来。这个顺序能帮你把问题隔离清楚:模块问题、接线问题、单片机问题,一次只查一个。跳过这步直接全接上,一旦不工作,你就得在三条链路上同时找茬,排查成本翻好几倍。
3. 配通 WiFi 模块:AT 指令集与透传模式配置序列
3.1 工作模式选型:为什么小车场景用 STA + TCP 客户端
ESP8266 这类模块的 AT 固件支持几种工作模式:Null(无 WiFi)、Station(连接路由器)、SoftAP(自己开热点)、AP+STA 共存。小车场景有两种典型组网方式。
第一种,家里或实验室有路由器,手机和模块都连同一个路由器,模块工作在 STA 模式,作为 TCP 客户端主动连手机或上位机开的 TCP 服务端。这种方式覆盖范围大、延迟稳定,实验包默认走这条路线。第二种,户外没有路由器,模块开 SoftAP,手机连模块的热点,此时模块自己就是 TCP 服务端,手机 App 作为客户端来连它。这种方式不需要额外设备,但模块的 AP 覆盖范围小,手机离车稍远就断。
我的建议是:课程设计、比赛展示优先用 STA 模式,因为路由器在场馆里基本都有,而且 STA 模式下模块的 IP 是路由器分配的,上位机连接逻辑更简单。实验包里如果默认给了手机 App 的 TCP 端口配置,也基本是按 STA 模式来的。选型上定了之后,剩下的事情就是让模块进入透传,把网络侧和串口侧打通。
3.2 可复制的 AT 指令序列与参数说明
下面是一套在 STA + TCP 客户端 + 透传模式下最常用的配置序列。注意不同固件版本的指令名称和参数略有差异,比如老固件用AT+CWJAP,新固件可能是AT+CWJAP配合不同引号格式;如果某条指令返回ERROR,先查你这块模块对应的 AT 指令手册,而不是怀疑线接错了。
# 1) 复位模块,回到命令模式,等待返回 ready AT+RST # 2) 设置为 Station 模式,0 是 NULL,1 是 STA,2 是 AP,3 是 AP+STA AT+CWMODE=1 # 3) 连接 2.4G 路由器或手机热点,只能连 2.4G AT+CWJAP="MyWiFi","password123" # 4) 查询模块自己的 IP,确认已经拿到地址 AT+CIFSR # 5) 建立 TCP 连接,"TCP" 表示协议,后面是服务端 IP 和端口 AT+CIPSTART="TCP","192.168.1.120",8080 # 6) 开启透传模式,1 表示透传,0 表示普通传输 AT+CIPMODE=1 # 7) 进入透传发送状态,之后串口收到的所有数据都会直接发出 AT+CIPSEND逐条说明参数。AT+CWMODE=1里的 1 表示 STA,如果后续要开热点做演示,改成 2 再配AT+CWSAP设置热点名和密码。AT+CWJAP的 WiFi 名和密码都要用英文双引号包起来,不支持中文 SSID,如果有中文热点名,先改路由器或手机热点名字。AT+CIPSTART里的目标 IP 是手机或上位机在路由器里分到的 IP,端口是上位机软件监听的端口,别搞反——模块是客户端,主动去连服务端,所以服务端要先启动监听。
AT+CIPMODE=1是透传开关,但注意开了透传不代表立刻进入透传,还需要AT+CIPSEND确认,模块返回>之后才进入透传状态。透传状态下,模块不再响应 AT 指令,所有串口数据都当作 TCP 负载发给服务端。要退出透传回命令模式,需要单独发一行+++,而且+++前后各留 1 秒不要发其他数据,否则模块会把它当成普通负载直接发出去。
提示:透传模式下模块不回
OK,是正常现象。很多同学在透传状态发AT收不到响应,就以为模块坏了,其实只是还停在透传里。
3.3 透传数据格式:回车换行、粘包与退出透传
TCP 是流协议,没有消息边界。手机 App 调用一次发送接口发了一个控制帧,模块可能把帧原样从串口吐出来,也可能把前后两次发送的数据合并成一段一次吐出来,这取决于网络栈的缓冲时机。这直接导致一个经典问题:STM32 串口中断可能一次收到多个指令的字节,如果单片机侧按「一次中断等于一条指令」来写,车子的行为就会完全错乱。
另一个问题是回车换行。很多网络调试助手发送数据时默认带\r\n,如果协议帧是纯二进制的,这两个字节混进来就是垃圾数据。我一般建议:上位机发送用十六进制格式,不要勾选「发送新行」,STM32 端采用帧头帧尾和校验来定位有效数据,而不是依赖换行符分隔。帧设计好了,粘包问题就不会影响解析正确性,最多是解析延迟一点点。
4. STM32 端 UART 接收与指令解析:从字节流到电机动作
4.1 接收方案选型:轮询、逐字节中断与空闲中断 DMA
F407 的 USART 接收有三种常见做法,新手容易一上来就选最复杂的。轮询方式在 main 循环里不断查__HAL_UART_GET_FLAG,代码简单,但 CPU 被频繁占用,而且容易丢数据;逐字节中断方式每次收到一个字节进一次中断回调,逻辑清晰、适合低速控制指令,小车遥控指令频率通常在 10 到 20 赫兹,这个方案完全够用;空闲中断加 DMA 是性能最优解,一轮批量搬数据、一次中断处理一包,适合数据量大的场景,但配置复杂,新手容易在 DMA 和空闲中断的配合上翻车。
我一般会选逐字节中断,理由是:指令短(一个帧才 6 个字节)、频率低、排查直观,中断里解析完直接置标志,主循环去执行,不会丢数据也不会卡死。等以后做图传、数据采集这类高吞吐场景,再换 DMA 方案不迟。代码里用 HAL 库的HAL_UART_Receive_IT每次收一个字节,收完回调里再重新开启下一次接收,形成一个持续的接收链。
4.2 帧协议设计:帧头、长度、命令与校验
裸字节流不能直接当指令用,必须定义帧协议。我的习惯是设计一个 6 字节的固定帧:帧头 2 字节(0xAA 0x55)、长度 1 字节、命令字 1 字节、参数 1 字节、校验 1 字节。帧头用两个字节是为了降低噪声下的误同步概率,单字节 0xAA 在干扰环境里太容易撞上。校验用异或,不用 CRC16,因为帧短、计算开销小,异或足够检测传输错误。
| 字节偏移 | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 内容 | 0xAA | 0x55 | len | cmd | param | sum |
| 含义 | 帧头1 | 帧头2 | 有效载荷长度 | 命令字 | 速度/转向参数 | 异或校验 |
sum 的计算方式是:0xAA ^ 0x55 ^ len ^ cmd ^ param,把帧头也纳入校验范围,可以顺带检测帧头是否被干扰。这样的协议不管是 WiFi 链路还是串口链路都能通用,手机 App 按这个格式打包 TCP 负载就行。
接收端用状态机解析,每来一个字节喂一次状态机,避免在中断里做复杂判断。代码如下:
/* frame_parse.h:基于状态机的帧解析,一次喂一个字节 */ uint8_t parse_state = 0; uint8_t frame_buf[6]; uint8_t frame_len; uint8_t frame_cmd; uint8_t frame_param; uint8_t frame_parse_byte(uint8_t b) { switch (parse_state) { case 0: /* 等待帧头1 */ if (b == 0xAA) { frame_buf[0] = b; parse_state = 1; } break; case 1: /* 等待帧头2 */ if (b == 0x55) { frame_buf[1] = b; parse_state = 2; } else { parse_state = 0; /* 帧头错误,重新同步 */ } break; case 2: /* 接收长度字段 */ frame_len = b; frame_buf[2] = b; parse_state = 3; break; case 3: /* 接收命令字 */ frame_cmd = b; frame_buf[3] = b; parse_state = 4; break; case 4: /* 接收参数 */ frame_param = b; frame_buf[4] = b; parse_state = 5; break; case 5: /* 接收校验并验证 */ frame_buf[5] = b; if ((frame_buf[0] ^ frame_buf[1] ^ frame_buf[2] ^ frame_buf[3] ^ frame_buf[4]) == b) { parse_state = 0; return 1; /* 返回1表示收到一帧完整数据 */ } parse_state = 0; break; default: parse_state = 0; break; } return 0; }这段代码的核心逻辑是:状态 0 和状态 1 负责锁定帧头,一旦第二个帧头不匹配就回到状态 0 重新找同步,这样即使数据流中间夹杂噪声也能自动恢复;状态 2 到状态 4 按顺序接收长度、命令和参数;状态 5 做异或校验,通过才返回 1。如果 len 字段可能大于 2,你需要把状态 3 之后改成按 len 循环接收,这里为了教学清晰用了固定帧结构。
串口中断回调里这样调用:
/* main.c:USART2 接 WiFi 模块,逐字节中断接收 */ uint8_t rx_byte; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { /* 每收到一个字节就喂给帧解析状态机 */ if (frame_parse_byte(rx_byte)) { /* 收到一帧完整数据,执行电机控制逻辑 */ motor_handle_cmd(frame_cmd, frame_param); } /* 重新开启下一次接收,形成持续接收链 */ HAL_UART_Receive_IT(&huart2, &rx_byte, 1); } }注意HAL_UART_Receive_IT必须在回调末尾重新调用,否则只收一次就停了。这是 HAL 库最容易漏的一步,漏掉之后的现象是:第一个指令有效,之后所有指令全部失效。Motor 控制函数在中断里直接调用,因为执行时间很短,只有几个寄存器写操作,不影响实时性;如果你在回调里做耗时操作(比如串口打印整个帧),就要改成置标志位、主循环执行。
4.3 指令到 PWM 的映射:电机驱动逻辑与速度控制
小车底盘通常用 TB6612 或 L298N 这类电机驱动板,控制逻辑差不多:两个方向引脚 IN1/IN2 决定电机转不转、往哪转,PWM 引脚决定转速。F407 的定时器输出 PWM,我一般用 TIM1 的通道 1 和通道 2 分别控制左右轮,GPIO 控制方向。代码结构如下:
/* motor.c:电机控制,PWM 占空比范围 0~1000 */ #define MOTOR_FORWARD 0 #define MOTOR_BACKWARD 1 #define MOTOR_STOP 2 /* 左右轮方向引脚初始化,此处假设 PB0 和 PB1 接驱动板 IN1/IN2 */ void motor_set(int left_pwm, int right_pwm, uint8_t dir) { if (dir == MOTOR_FORWARD) { GPIOB->BSRR = GPIO_PIN_0 | GPIO_PIN_1; /* IN1=1 IN2=1 前进 */ } else if (dir == MOTOR_BACKWARD) { GPIOB->BRR = GPIO_PIN_0 | GPIO_PIN_1; /* IN1=0 IN2=0 后退 */ } else { GPIOB->BRR = GPIO_PIN_0 | GPIO_PIN_1; /* 停车 */ } __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, left_pwm); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, right_pwm); } void motor_handle_cmd(uint8_t cmd, uint8_t param) { switch (cmd) { case 0x01: /* 前进:左右轮同速 */ motor_set(param, param, MOTOR_FORWARD); break; case 0x02: /* 后退:左右轮同速 */ motor_set(param, param, MOTOR_BACKWARD); break; case 0x03: /* 左转:左轮慢、右轮快 */ motor_set(param / 2, param, MOTOR_FORWARD); break; case 0x04: /* 右转:右轮慢、左轮快 */ motor_set(param, param / 2, MOTOR_FORWARD); break; case 0x05: /* 停车 */ motor_set(0, 0, MOTOR_STOP); break; default: break; } }这里的 param 直接当作 PWM 比较值使用,范围 0 到 1000。左转和右转用了简单差速:一侧减半速,另一侧保持,转弯效果比原地转圈更柔和。如果你想要原地旋转,可以让左轮正转、右轮反转,那需要额外控制方向引脚的状态组合,不同驱动板接线不一样,需要按驱动板的真值表调整。还有一件事容易漏:PWM 通道必须先在初始化时调用HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1)和通道 2,否则__HAL_TIM_SET_COMPARE写了也不输出波形。很多人卡在车不转,查了半天电机驱动板,最后发现定时器 PWM 输出根本没启动。
5. WiFi-UART 实验避坑指南:5 个最常翻车的细节
5.1 一上电就复位:WiFi 模块把 3.3V 拉垮了
现象:模块和 F407 一上电,整车反复重启,OLED 屏闪一下就没,调试串口打印乱码或者干脆没输出。
原因:这是供电问题。ESP8266 这类模块在射频发射的瞬间电流会冲到 300mA 以上,直接从 F407 板载 3.3V 取电的话,电源电压被瞬间拉低,F407 的复位电路检测到欠压就重启。重启之后模块重新连接 WiFi,又触发一次大电流,形成死循环。
解决:模块单独供电。从 5V 经过稳压到 3.3V,或者用一个独立 3.3V 电源,GND 与 F407 共地。检查方法很简单:万用表测模块 VCC 引脚,在发射瞬间看电压有没有明显跌落。这是我做第一版时踩得最惨的坑,后来所有带 WiFi 模块的项目,第一步先把供电独立出来。
5.2 AT 指令不回 OK:波特率与透传残留两个原因
现象:串口助手发AT\r\n,模块没有任何响应,或者回一堆乱码。
原因:两个。一是波特率不匹配,模块固件默认波特率可能是 115200,也可能是 9600,如果模块买来被改过默认值,你按 115200 发就对不上。二是模块还停在透传模式,透传状态下 AT 指令会被当作普通数据直接发出去,模块自然不回复。
解决:先用串口助手轮询波特率,从 9600 到 115200 到 74880 逐个试;如果模块在透传状态,单独发+++且前后各停顿 1 秒,退出透传后再发 AT。检查模块是否退出透传有个小技巧:发 AT 之后等 1 秒,如果收到OK就是命令模式,收到乱码或者什么都没有就是还在透传或者波特率不对。
5.3 一次收到多个指令:TCP 缓冲导致的粘包问题
现象:手机 App 明明只发了一次前进指令,车却执行了两次甚至三次,或者车的行为像是把上一次的指令又执行了一遍。
原因:TCP 是字节流,没有消息边界。模块从网络收到数据后,可能等积累了足够多的字节再一次从串口全部吐出来,也可能把两次发送的数据合并成一次吐出。STM32 端如果假设一次中断等于一条指令,就会把多帧数据当成一帧处理,解析逻辑直接错乱。
解决:必须按帧协议解析,不能按中断次数解析。用 4.2 的状态机方案,每收到一个字节就喂给状态机,一帧校验通过才执行一次指令。这样无论模块一次吐 6 个字节还是 12 个字节,最终都能正确切分出每一帧。我还会在 App 端发送时用十六进制格式、关闭「发送新行」,减少换行符混入。
5.4 车不转:PWM 通道没 Start 和使能引脚没接对
现象:WiFi 已连接、串口有数据、指令解析也正常打印了,但电机就是不动。
原因:最常见的是两个。一是定时器 PWM 通道只设置了比较值,但没调用HAL_TIM_PWM_Start,GPIO 上没有波形输出;二是驱动板的使能引脚(如 TB6612 的 STBY,L298N 的 ENA/ENB)没接高电平,驱动板整个被禁用,PWM 信号到了但电机得不到电。
解决:先分层定位。用示波器或逻辑分析仪量 F407 的 PWM 引脚,看有没有波形;再看驱动板使能引脚电压是不是高电平;最后量驱动板输出端有没有电压。没有示波器的话,可以把定时器预分频调大、占空比调到 50%,用万用表测引脚电压,如果接近 1.65V(3.3V 的一半),说明 PWM 在输出。多数时候,问题就出在HAL_TIM_PWM_Start这行代码上。
5.5 手机连不上模块:5G 频段和 AP 隔离两个隐蔽坑
现象:手机能搜到家里的 WiFi,模块也配置成连接同一个 WiFi,但 App 就是连不上模块的 IP 和端口。
原因:两个可能性。一是路由器开启了 5G 频段,模块只支持 2.4G,手机连了 5G、模块连了 2.4G,两个设备不在同一网段;二是路由器开启了 AP 隔离,无线设备之间禁止互相访问,手机和模块虽然都在同一 WiFi 下,但 IP 层不通。
解决:确认手机连接的是 2.4G 频段的热点,把路由器双频合一功能关掉,或者用手机开一个 2.4G 热点给模块连;登录路由器管理页,关闭 AP 隔离选项。验证方法:模块AT+CIFSR拿到 IP 后,手机用浏览器访问这个 IP 的端口,通不通一目了然。这条坑在实验室路由器上尤其常见,因为实验室网络往往默认开了隔离策略。
6. 链路验证与进阶方向:用回环测试和心跳包把实验做实
6.1 三级验证:从回环测试到整车联动
我拿到一块新板子或新模块,从来不会直接整车跑,而是按三级验证走,每级只验证一段链路。第一级,模块单独接 USB-TTL,手机 App 或电脑上的网络调试助手连上模块,发一个十六进制帧,看串口助手是否原样收到——这验证 WiFi 到 UART 的链路通不通。第二级,F407 不接 WiFi,用 USB-TTL 往 USART2 发同样的帧,看调试串口打印的解析结果是否正确——这验证 UART 到指令解析的链路。第三级,全部接上,从手机发帧,听电机声音、看 PWM 波形、观察轮子转向,做整车联动验证。每一级出问题,都只在一个环节里找,半小时能定位,否则盲查可能耗掉一整天。
6.2 值得做的进阶:心跳包、协议版本与断线重连
基础实验跑通之后,有三个进阶点值得投入。第一,在协议里加类型字段,比如 0x01 是控制帧、0x02 是心跳帧、0x03 是参数配置帧,让同一个串口通道能复用。第二,加上指令超时保护:F407 记录最后一次收到有效指令的时间,超过 500ms 没收到新指令就自动停车。这个机制看着简单,实际救了我好多次——WiFi 偶尔抖动一下,App 卡顿一下,车如果没有超时保护就会保持上次的动作一直冲出去。实现也很轻,主循环里用HAL_GetTick()判断一下即可。第三,模块侧做断线重连,在AT+CIPSTART失败时延时重试,避免现场演示时断一次就全盘瘫痪。
最后说个习惯:我后来做任何串口类项目,都会先把打印日志打开,每个关键节点留一行输出。指令收到、帧校验通过、PWM 设置完成,各打一条,配合调试串口看流程走到哪一步中断了,问题就水落石出。WiFi-UART 这类实验的价值不只是让车跑起来,而是让你亲手把「无线链路」和「有线串口」这两套体系用一条透明管道接起来——这件事想明白了,以后做任何联网设备都心里有底。希望帮到你。
本文还有配套的精品资源,点击获取