
简介这是一份基于STM32 HAL库的智能小车WIFI遥控工程面向嵌入式初学与DIY玩家解决用ESP8266实现手机遥控小车的问题涉及串口DMA传输、串口输出、ESP8266配置等关键点。资源共78个文件以48个H头文件与21个C源文件为主含HAL库驱动、USART/DMA初始化、GPIO控制等源码另有hex固件、MDK工程、ioc配置、启动文件及说明压缩包仅554KB结构清晰。已有2625人浏览学习。说明文档给出接线方式USART1接ESP8266、USART2输出及热点/TCP server配置思路借助完整工程可快速复现WIFI遥控小车实验理解DMA与ESP8266通信机制为扩展蓝牙等遥控方式打下基础。1. 先理清楚整体思路为什么选ESP8266跟STM32怎么配合1.1 多走一步的硬件选型先回答一个很多人问的问题做wifi遥控小车为什么选ESP8266而不是蓝牙模块、NRF24L01或者干脆上4G蓝牙模块HC-05、HC-06确实更便宜、配对方便但有两个硬伤一是通信距离太短手机离开两三米就开始丢包二是一对一连接后续想加摄像头、想做成局域网多设备控制基本只能换方案。NRF24L01距离还行但需要额外的接收端插在电脑上没法直接用手机控制体验差一截。4G模块成本高、调试流程重学习阶段没必要。ESP8266的优势在于模块十几块钱、资料遍地都是、支持TCP/IP协议栈手机连上同一个路由器就能直接通信。更重要的是它跟STM32之间只有一根串口线通过AT指令就能把网络数据透传成串口数据这让整个系统的角色划分非常清晰ESP8266只负责上网传数据STM32只负责解析指令控制电机。也就是用最简单的方式拿到最实用的结果。1.2 系统架构里的三个环节以一个典型的局域网遥控场景为例整个控制链路分成三段控制端手机或者PC装一个TCP调试助手类APP作为TCP客户端去连接小车的IP和端口。传输端ESP8266工作在Station模式连上家里的路由器然后作为TCP服务端监听一个端口。手机连上来之后ESP8266把网络收到的字节流原样转发到串口TX把STM32发来的串口数据原样转发到网络。执行端STM32通过串口中断接收ESP8266转发的指令按照约定好的帧协议解析出前进、后退、左转、右转、停止、调速等命令然后通过PWM控制电机驱动模块。这三段里最核心也最容易出问题的是第二段——ESP8266和STM32之间的串口对接。因为网络传输是不可控的手机发出去的指令什么时候到达、中间会不会丢包都没法保证。所以STM32端的串口接收不能只写一个简单的查询函数而要用中断加缓冲区的方案保证数据随时来随时接、不丢字节。我自己做这个项目时把模块分层测试了一遍明显比一次性全接好再调要省事得多。建议你也按ESP8266单独测通网络链路→STM32单独测通串口命令→联调这个顺序来。2. 硬件接法和初始化里容易踩的坑2.1 引脚分配和电平匹配我用的STM32型号是F103C8T6板子便宜、性能足够。串口资源一共三个USART分配如下外设引脚用途波特率USART1PA9(TX)、PA10(RX)调试信息打印接ST-Link虚拟串口57600USART3PB10(TX)、PB11(RX)与ESP8266通信115200TIM2PA0~PA3输出四路PWM控制电机20kHz这里有一个非常关键的细节ESP8266模块的VCC要接3.3V不是5V。很多ESP8266模块板载了稳压芯片标称支持5V输入但如果你用的是那种没有稳压的裸模块直接接5V大概率会烧掉。稳妥的做法是看模块丝印和店家说明没写支持5V就一律3.3V供电。另外注意ESP8266的串口逻辑电平是3.3VSTM32的IO口也兼容3.3V所以两者之间可以直接连接不需要电平转换。但千万别把ESP8266的TXD接到STM32的3.3V引脚之外的5V容忍引脚上——虽然F103的PA口标称5V容忍但在这种低压模块互联场景最好还是按3.3V逻辑来减少隐患。2.2 串口接收中断的初始化细节用HAL库的串口中断接收时有个容易忽略的地方HAL_UART_Receive_IT每次只能接收一个字节接收完成后会触发中断回调然后停止接收。如果你不重新调用一次HAL_UART_Receive_IT后续数据就再也进不来了。所以标准的做法是在回调函数里重新开启接收同时把收到的一个字节压入环形缓冲区uint8_t rx_buffer[128]; volatile uint8_t head 0; volatile uint8_t tail 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART3) { uint8_t tmp huart-Instance-DR; uint8_t next (head 1) % 128; if (next ! tail) { rx_buffer[head] tmp; head next; } HAL_UART_Receive_IT(huart, tmp, 1); } }注意这里一定要判断缓冲区是否已满如果head追上了tail就丢弃新数据而不是覆盖旧数据。否则在极端情况下主循环还没取走数据缓冲区就被新数据覆盖了你会看到小车偶尔乱动一下又恢复很难排查。还有一个常见的坑HAL_UART_Receive_IT初始化时传入的接收缓冲区地址必须是一个长期有效的变量不能是局部变量的地址。很多人写的时候会这样做while (1) { uint8_t data; HAL_UART_Receive(huart3, data, 1, 100); }这种查询方式不是不能用但在wifi遥控场景下手机指令是异步到达的主循环如果正在做其他耗时操作比如处理传感器数据就会漏掉串口数据。所以一定要用中断接收。3. 代码实现从AT指令到数据协议3.1 自定义一套指令帧协议ESP8266透传上来的是数据流STM32要能从字节流里识别出一条完整的指令。最简单的做法是帧头 数据 帧尾 校验。我用的协议格式是这样的字节序号内容说明00xAA帧头110x55帧头22direction方向指令0x01前进0x02后退0x03左转0x04右转0x00停止3speed速度值0~1004checksum校验和第2字节到第3字节累加取低8位帧头用两个字节0xAA 0x55是为了降低误判概率。解析时用状态机不依赖主循环去逐个字节比对typedef enum { IDLE, GOT_FRAME_HEADER_1, GOT_FRAME_HEADER_2, GOT_DIRECTION, GOT_SPEED } ParseState; void parse_byte(uint8_t byte, Command *cmd) { static ParseState state IDLE; static uint8_t dir, spd; switch (state) { case IDLE: if (byte 0xAA) state GOT_FRAME_HEADER_1; break; case GOT_FRAME_HEADER_1: if (byte 0x55) state GOT_FRAME_HEADER_2; else if (byte 0xAA) state GOT_FRAME_HEADER_1; else state IDLE; break; case GOT_FRAME_HEADER_2: dir byte; state GOT_DIRECTION; break; case GOT_DIRECTION: spd byte; state GOT_SPEED; break; case GOT_SPEED: if ((dir spd) byte) { cmd-direction dir; cmd-speed spd; // 这里置标志位主循环处理 } state IDLE; break; } }校验和很简单就是方向加速度。这个协议在局域网场景下丢包概率很低所以没加更多冗余。如果你后续要换成公网传输建议把校验改成CRC8甚至CRC16别用累加和了。3.2 ESP8266的初始化配置ESP8266的配置本质就是发AT指令。我需要通过USART3跟它通信但这个时候USART3没有接STM32的中断接收吗其实可以复用。在初始化阶段用一个简单的发送命令、等待OK响应的函数HAL_StatusTypeDef send_at_command(UART_HandleTypeDef *huart, const char *cmd, uint16_t timeout) { uint8_t buf[64]; uint16_t idx 0; memset(buf, 0, sizeof(buf)); HAL_UART_Transmit(huart, (uint8_t *)cmd, strlen(cmd), 2000); HAL_UART_Transmit(huart, (uint8_t *)\r\n, 2, 1000); uint32_t start HAL_GetTick(); while (HAL_GetTick() - start timeout) { while (__HAL_UART_GET_FLAG(huart, UART_FLAG_RXNE)) { uint8_t byte huart-Instance-DR; buf[idx] byte; if (idx sizeof(buf)) break; } if (strstr((char *)buf, OK)) return HAL_OK; if (strstr((char *)buf, ERROR)) return HAL_ERROR; } return HAL_TIMEOUT; }这个函数的意思很直白发一条指令然后在超时时间内等待模块返回OK或ERROR。有一个容易被忽略的点AT指令的每条命令结尾必须带\r\n也就是回车换行官方固件才认。初始化流程一步都不能省send_at_command(huart3, ATE0, 500); // 关闭回显 send_at_command(huart3, ATCWMODE1, 1000); // 配置Station模式 send_at_command(huart3, ATCWJAP\MyWiFi\,\12345678\, 8000); // 连接路由器 send_at_command(huart3, ATCIPMUX0, 1000); // 单连接模式 send_at_command(huart3, ATCIPSTART\TCP\,\0.0.0.0\,8080, 3000); // 这个要改成实际服务端IP send_at_command(huart3, ATCIPMODE1, 1000); // 进入透传模式 send_at_command(huart3, ATCIPSEND, 1000); // 开始发送数据这里注意几个细节ATCIPSTART使用的是Client模式意思是ESP8266作为TCP客户端去连接一个服务端。如果你的控制端是手机APP那APP一般作为TCP客户端ESP8266作为TCP服务端这时要用ATCIPSERVER1,8080。具体方向取决于你用什么控制软件。用TCP调试助手类APP时通常让ESP8266做服务端APP去连它。连接WiFi的指令耗时可能长达5~10秒有些路由器反应慢超时时间给够别省。进入透传模式之后串口收到的数据会原样发到网络网络收到的数据会原样从串口出来。这是最省心的模式STM32不需要知道任何TCP细节只需要处理字节流。3.3 电机控制端的接入电机驱动我用的TB6612比L298N体积小、效率高接线也更简洁。STM32只需输出两路PWM控制左右轮的速度再用四个GPIO控制方向。HAL库配置TIM2的四路PWM输出初始化代码如下void Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); htim2.Instance TIM2; htim2.Init.Prescaler 720 - 1; // 72MHz / 720 100kHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 100 - 1; // 100kHz / 100 1kHz htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim2); }这里PWM频率选1kHz对直流减速电机来说足够平滑也不会产生明显的啸叫。占空比对应速度值100%占空比就是最大速度。解析到指令后直接调用调整占空比的函数就行。4. 联调中出现的高频问题4.1 AT指令完全不回这个绝对是遇到最多的问题。ESP8266的TXD接上STM32的RXDSTM32发AT模块毫无反应串口调试助手一片空白。排查步骤按顺序来先确认模块供电。3.3V模块实测电流可能到300mA有些开发板的3.3V引脚输出能力不够会电压跌落导致模块不启动。这时候用万用表量一下模块VCC对地电压如果低于3.0V就有问题。确认EN/CH_PD引脚有没有接高。有些模块这个引脚需要拉高才工作悬空的话模块上电后不启动。确认TX和RX是否交叉。USB转TTL的TX接ESP8266的RXRX接ESP8266的TX这是基本常识但接错率常年居高不下。确认波特率。ESP8266出厂波特率一般是115200但也可能是9600某些固件或旧模块。先用USB转TTL配合电脑串口助手扫一下常见波特率找到能返回OK的那个。4.2 能AT但连不上路由器ATCWJAP返回ERROR或一直0通常就是三个原因密码错误这个最蠢检查大小写和特殊字符。注意密码里有引号的话要转义。路由器只开了5G频段。ESP8266只支持2.4GHz5G频段搜不到、连不上。把路由器设置成双频混合或者让手机开2.4G热点测试。路由器开启了防蹭网、MAC地址过滤。把ESP8266的MAC地址加白名单。还有一个容易忽略的点ESP8266连接路由器后它自己会有一个IP地址。你要在ATCIPSTART或者CIPSERVER之前先通过ATCIFSR查询到它当前的IP控制端才能连过去。很多人忘了这一步在APP里填了个错误的IP怎么都连不上。4.3 能连通TCP但小车乱动或不响应TCP通上之后最大概率的坑出现在控制端和STM32的协议不一致。我调试的时候用串口调试助手直接监控USART3收到的原始字节发现手机APP发出去的是字符串0xAA 0x55 0x01 0x50 0x51这种十六进制字节但某些APP默认发的是文本会把字符串转成ASCII码再送去网络。比如你发AA55四个字符到达STM32端就成了0x41 0x41 0x35 0x35解析器当然不认识。解决方案有两个一是APP端发送时选择Hex发送严格按十六进制发送二是在STM32端再加一层字符解析把收到的ASCII字符串AA55015051转换成二进制。我更推荐前者因为代码简单而且链路越透明越好排查。4.4 遥控延迟太大如果发现按一下手机按钮小车要过几百毫秒才动需要从三个层面排查网络环境。手机和ESP8266要连同一个路由器尽量避免跨网段。如果用手机热点ESP8266连热点手机再连热点这种模式下延迟通常比路由器模式高一些但还能接受。TCP保活。ESP8266的透传模式下如果长时间没数据TCP连接可能被路由器或运营商断开。断开后发数据就要重连延迟巨大。解决方法是让STM32周期性发一个心跳包比如每秒发一个0x00保持TCP连接存活。STM32主循环处理速度。如果主循环里有耗时的阻塞操作比如HAL_Delay(100)串口数据即使到了解析也可能滞后。建议主循环只做检查标志位、处理指令、更新PWM把耗时操作放到中断或者定时器里。5. 手机控制端怎么搞定很多教程到这里就停了但遥控小车缺了控制端前面全部白搭。控制端有三种做法从简到繁5.1 直接用网络调试助手APP在应用商店搜TCP调试助手或者网络调试助手选择TCP Client模式填上ESP8266的IP和端口号比如192.168.1.100:8080然后就可以发送控制指令了。这种方案的好处是零开发缺点是按钮是文本输入框操作手感一般。5.2 自己做一个简单的Android APP用Android Studio写一个简单的TCP客户端界面上放置五个按钮前进、后退、左转、右转、停止和一个SeekBar调速度每个按钮点击时通过Socket发送对应的十六进制指令帧。这个方案适合想练手Android网络编程的同学。核心代码很短大概就是new一个Socket连接到ESP8266的IP和端口然后拿到OutputStream写字节。5.3 用微信小程序/网页控制如果想彻底免安装可以用微信小程序的TCP Socket能力wx.connectSocket或者网页的WebSocket连接ESP8266。这里需要稍微注意一点ESP8266默认AT固件原生支持TCP不支持WebSocket所以网页方案通常要加一层协议转换复杂度明显提升。我的建议很简单第一版先用TCP调试助手类APP把整体链路跑通确认没问题后再决定要不要自己写控制端。别一上来就搞完整APP链路不通的话你根本分不清是APP代码的问题还是小车固件的问题。6. 实际调试中的几条深刻教训前面讲的都是方法论最后分享几条我做这个项目的具体体会。第一分模块测试是刚需。ESP8266和STM32先用各自的测试手段验证通再联调。我见过很多人在联调阶段花了几个晚上最后发现其实是ESP8266模块本身没工作这种挫败感特别伤积极性。自己的模块上电后用USB转TTL连电脑单独测一遍确认能收到OK再接到STM32上排查范围立刻缩小一半。第二串口调试助手的日志不要乱关。即使在STM32的调试串口USART1上打印原始解析结果遇到问题能顺藤摸瓜。我习惯用一段枚举数组把解析状态打印出来第一时间就能看出来是协议帧没同步还是校验不对。第三透传模式下如果出现串口数据黏包——就是STM32一次收到多帧数据比如手机连点两下前进ESP8266可能把两帧合并成一次串口发送。状态机解析方案天然能处理这种情况因为是逐字节解析而不是按固定长度接收这也是为什么我坚持用状态机而不是阻塞地HAL_UART_Receive。第四电源是个隐形炸弹。PWM满占空比时电机启动电流很大如果电池电压不稳定会导致ESP8266偶尔重启。解决办法是电机电源和逻辑电源分开至少要在电机电源端并一个大电容470uF以上实测能显著减少重启概率。这个wifi遥控小车做完之后你其实已经把STM32的串口通信、中断处理、PWM控制、状态机解析以及ESP8266从AT指令到透传模式的完整链路都过了一遍。后面想扩展的话加个摄像头做图传、接入MQTT做远程控制、或者换成自己的TCP服务端做数据记录都是从这个基础上长出来的。技术这东西通路跑通一次后面的路就顺了。本文还有配套的精品资源点击获取