
简介这是一份基于 STM32F103 与 HAL 库的智能小车 WiFi 遥控参考工程适合学习嵌入式串口通信、DMA 传输和 ESP8266 应用的开发者。核心逻辑是 USART1 连接 ESP8266 模块、USART2 输出调试信息利用串口 DMA 完成数据收发手机连接 ESP8266 建立的热点后用网络调试助手设为 TCP server 发送指令即可控制小车运动。压缩包共 78 个文件以 48 个 h 头文件和 21 个 c 源文件为主另含可直接烧录的 hex 固件、MDK 工程 uvprojx、CubeMX 配置 ioc、bat 脚本和详细说明 txt整体仅 554KB目录结构清晰便于移植到自己的小车项目中。目前已有 2625 人学习下载对于正在做课设或竞赛小车的开发者尤其有参考价值。资料不仅提供完整可编译源码和固件还保留 HAL 库初始化、main 主逻辑及 USART/DMA 驱动代码适合参考 WiFi 遥控小车的接线与通信流程也可以在此基础上扩展手机 App、TCP 指令协议或更多控制功能。 做小车做到第4篇我终于把拖在车后面的那根串口线给砍掉了。前几篇调电机、调PWM、调循迹每次调试都抱着USB转TTL线在桌面上跟着车跑来跑去小车一启动就把杜邦线扯飞传感器数据动不动就断那个画面搞过车模的人应该都懂。这篇要解决的就是这个老痛点用ESP8266做WiFi遥控STM32走HAL库开发手机连上小车自己发出来的WiFi发几个字节过去小车就动、就停、就拐弯。整个过程不依赖路由器、不依赖外部网络适合已经用HAL库点亮过灯、驱动过电机、跑通过串口收发的小伙伴继续往下玩。文章会从方案选型、ESP8266单独调通、STM32侧中断接收与帧解析、运动控制映射、再到实际跑车遇到的坑一条完整链路写下来代码可以直接抄踩过的坑也都标出来了。1. 为什么这个阶段才上WiFi先有线调稳再无线遥控1.1 蓝牙、2.4G、WiFi三选一智能小车的无线遥控方案市面上绕不开三样蓝牙模块、NRF24L01、ESP8266。我在这台小车上都试过或者调研过先把对比摆出来方案代表模块优点缺点适合场景蓝牙HC-05/HC-06配对方便手机直接当上位机串口透传代码简单距离短一对一连接扩展性差近距离把玩、学习串口透传2.4GNRF24L01低延迟抗干扰好功耗低手机没法直接连需要配手柄或另一块单片机开发成本高遥控车、遥控飞机WiFiESP8266便宜自带协议栈手机和电脑都能直连可扩展局域网控制AT指令配置绕TCP数据有粘包供电敏感智能小车、物联网入门、远程监控NRF24L01虽然延迟最低但电脑和手机都收不了它得另外搞一套收发端对有上位机控车需求的人来说不友好。蓝牙方案在近距离场景下也能用但第4篇了我不太想再做“串口线换成了蓝牙串口”这种毫无新意的事而且后面想接摄像头、想在网页或APP上控车蓝牙基本就要推翻重来。ESP8266赢在生态成熟十几块钱一片内部自带完整的TCP/IP协议栈今天要做遥控跑AT指令就行不需要动它的SDK。以后想升级还能直接拿它当MCU用灵活性最高。这就是我这台车选它的原因。1.2 整条链路手机到轮子之间发生了什么在写代码之前先把数据流理清楚这步不做后面全乱套。整体链路长这样手机TCP Client → WiFi 直连 ESP8266 发出的热点 → ESP8266 工作在 AP 模式 TCP Server → UART2 串口把数据发给 STM32F103 → 控制驱动芯片TB6612/L298N → 左右电机这里有个细节ESP8266做的是Access Point也就是说它自己发出一个WiFi热点手机去连这个热点然后手机作为TCP客户端连接8266上开启的TCP服务端IP默认是192.168.4.1端口随便定我用的8080。这样做的好处是彻底摆脱路由器在实验室、操场跑都能用。坏处是手机连了小车热点之后就暂时上不了外网对遥控场景没有任何影响。数据流向是双向的手机发指令进ESP8266ESP8266通过串口把数据吐给STM32STM32如果需要回传状态也能通过同一根串口把数据发给ESP8266再由8266经WiFi转发回手机。这套链路做出来的效果就是手机上一个普通的“网络调试助手”就能当遥控器用。2. 先把ESP8266单独调通AT指令配置与固件版本坑2.1 调试工具与接线第一次接触ESP8266别直接往STM32上焊大概率翻车。我建议先用USB转TTL模块单独把8266这条链路调通确认网络连接和数据收发都正常了再把它接到STM32上。接线方式ESP8266引脚USB转TTL对应VCC3.3V不要接5VGNDGNDTXRXRXTXEN/CH_PD3.3V使能引脚必须拉高新手最容易栽的地方是EN脚悬空导致模块根本没启动串口助手发AT指令石沉大海。还有供电问题USB转TTL模块上自带的3.3V输出电流通常只有几十毫安够是够但8266在发射WiFi瞬间电流会猛增如果USB口供电弱模块会反复重启。我用的是单独一个AMS1117-3.3稳压小板给8266供电再跟USB转TTL共地稳得很。操作流程USB转TTL插电脑打开串口助手波特率115200因为8266出厂默认115200发一个AT收到OK就说明模块活着。这里有个经验如果发AT没反应先看看模块上有没有一个叫CH_PD或者EN的引脚用手头杜邦线把它接到3.3V再说八成是这个问题。2.2 从AT到TCP Server的完整配置序列8266的AT配置网上教程一堆但很多没把每条指令干什么讲清楚。我按我这台车的实际配置列一下每条都标注了作用AT # 测试通信 OK ATE0 # 关闭回显省得串口数据里全是垃圾干扰STM32侧解析 OK ATCWMODE2 # 设为AP模式SoftAP让8266自己发WiFi热点 OK ATRST # 重启使配置生效 OK ATCIPMUX1 # 开启多连接模式只有这个模式下才能开TCP Server OK ATCIPSERVER1,8080 # 启动TCP Server端口8080 OK配完之后手机上打开WiFi设置能看到一个类似AI-THINKER_xxxx的热点连上去。然后打开手机上的“网络调试助手”或者“TCP调试工具”APP新建TCP Client连接地址填192.168.4.1端口填8080连接成功后用串口助手给8266发任意字符串手机会立刻收到说明数据通路已经打通。这里要重点强调一个坑ATCIPMUX1下8266不支持透传模式ATCIPMODE1只能在CIPMUX0的单连接模式下使用。所以不要傻乎乎地去执行ATCIPMODE1会卡在配置失败或者行为异常。既然我们要用Server模式做遥控就老老实实解析8266串口输出的数据头。2.3 8266吐出来的数据长什么样当手机通过TCP连接给8266发数据时8266的串口输出不是直接把数据透传出来而是带了一个头格式如下IPD,通道ID,数据长度:实际数据举个例子手机发送了5个字节串口上会看到IPD,0,5:AA0155FB后面跟着的是原始二进制数据这里用十六进制表示方便看。这个头看着烦但处理起来也不难我们在STM32侧找的帧头是0xAA而这些IPD,0,5:全是ASCII字符十六进制是0x2B、0x49、0x50这些跟0xAA八竿子打不着所以只要在接收状态机里等0xAA出现就行自动就把前缀跳过去了。基于这个特性我在STM32里没有去解析IPD头直接找帧头。实践证明简单可靠不用费劲去提取长度字段。3. STM32串口侧接收HAL库中断接收与遥控帧解析3.1 USART2接ESP8266USART1留作调试串口资源分配上我建议USART1接电脑调试口USART2接ESP8266。这个分配方案的原因很简单调试信息和WiFi数据分开避免互相干扰。真机上跑的时候USART1可以用来打印接收到的指令和解析结果帮助定位问题一切正常后再拔掉调试线也不影响运行。CubeMX里只需把USART2模式设为Asynchronous波特率设为115200——跟8266串口默认波特率一致。没错我并不建议像很多人那样非要用9600。8266默认115200保持默认就少改一处配置等遇到干扰问题再降波特率也不迟后面第5章会展开。初始化代码基本不用改MX_USART2_UART_Init()生成好之后在main里开启首次接收中断。3.2 帧协议设计为什么要有帧头和校验TCP是流式协议数据在WiFi链路里会粘包。比如手机上快速点了三下前进这三次指令可能封装成一个TCP包到达8266再由串口一次吐给STM32。如果没有协议边界STM32根本分不清哪几个字节属于一条指令。另外TCP有重传机制极端情况下字节顺序也可能被打乱。所以遥控帧必须定义得足够严谨。我设计的帧格式一共5个字节短小精悍适合实时控制字节序号内容说明00xAA帧头固定值1cmd指令0x01前进0x02后退0x03左转0x04右转0x05停止2speed速度值0~1003sum校验和 (0xAA cmd speed) 0xFF40x55帧尾固定值一个有效指令示例AA 01 50 FB 55表示前进、速度800x50、校验和0xFB0xAA0x010x500xFB。很多人说“就一个遥控而已加什么校验”但实际跑起来你就明白了——电机启动瞬间的电磁干扰完全可能让串口收到一个翻转的字节。没有校验一条指令就变成另一条指令小车向前冲变成向左拐这是要撞墙的。有了校验坏帧直接丢弃最多让小车丢一条指令不会乱动。3.3 中断接收状态机代码接收我用最原始也最稳的方式HAL_UART_Receive_IT逐字节中断接收每收一个字节进一次回调在回调里跑状态机。数据量就每帧5字节完全够用。typedef enum { WAIT_HEAD, WAIT_CMD, WAIT_SPEED, WAIT_SUM, WAIT_END } RxStep; uint8_t rx_byte; // 中断接收单字节缓冲 volatile RxStep rx_step WAIT_HEAD; volatile uint8_t rx_cmd 0; volatile uint8_t rx_speed 0; volatile uint8_t rx_sum 0; volatile uint8_t frame_ok 0; // 一帧有效数据就绪标志 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { switch (rx_step) { case WAIT_HEAD: if (rx_byte 0xAA) rx_step WAIT_CMD; break; case WAIT_CMD: rx_cmd rx_byte; rx_step WAIT_SPEED; break; case WAIT_SPEED: rx_speed rx_byte; rx_step WAIT_SUM; break; case WAIT_SUM: rx_sum rx_byte; rx_step WAIT_END; break; case WAIT_END: if (rx_byte 0x55 rx_sum ((0xAA rx_cmd rx_speed) 0xFF)) { frame_ok 1; } rx_step WAIT_HEAD; break; } // 开启下一次单字节接收 HAL_UART_Receive_IT(huart2, rx_byte, 1); } }主循环里的消费逻辑while (1) { if (frame_ok) { frame_ok 0; car_control(rx_cmd, rx_speed); } }这个状态机的妙处在于即使中途因为干扰收到乱码比如本该在最后一个字节位置收到0x55却收到了0x00状态机会直接回到WAIT_HEAD重新找帧头下一帧数据不受影响。TCP粘包导致一帧后面紧跟另一帧也一样处理反正帧与帧之间是严格按字节边界走的状态机能自动切帧。另一个关键点是不要在中断回调里直接做电机控制函数。中断回调应该越短越好只做数据搬运和标志置位具体的运动控制在主循环里做。原因很实际电机控制涉及GPIO写和PWM比较值设置如果在中断里插入这些操作万一此时又来了另一个串口中断或其他定时器中断优先级处理不当就会丢字节。4. 指令到轮子运动控制层怎么接4.1 电机驱动与PWM初始化小车底盘一般配两个直流减速电机驱动芯片我用的是TB6612FNG相比老前辈L298N优点是小巧、压降小、PWM频率可以跑更高。驱动部分接线大致如下STM32的PA4、PA5控制左电机方向AIN1、AIN2STM32的PA6、PA7控制右电机方向BIN1、BIN2TIM3的CH1PA6复用输出左电机PWMCH2PA7复用输出右电机PWM如果你换用L298N也完全一样的逻辑无非是IN1~IN4控制方向、ENA/ENB接PWM。注意TB6612的PWMB和PWMA口是独立使能两个通道都要各自初始化。CubeMX里TIM3配置预分频PSC设为71自动重装载值ARR设为999这样PWM频率是72MHz / (72 * 1000) 1kHz。这个频率对直流减速电机和TB6612都合适电机噪音也不大。初始化代码如下HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2);速度范围0~100通过占空比映射到CCR比较值。ARR是999所以速度百分比直接乘10就是比较值不用额外做比例换算。4.2 car_control函数的核心逻辑方向控制和速度控制分开处理方向是GPIO电平速度是PWM占空比二者解耦。这里给出核心实现void car_control(uint8_t cmd, uint8_t speed) { if (speed 100) speed 100; // 限幅防止意外数据把PWM写满 uint32_t ccr (uint32_t)speed * 10; // 0~100 映射到 0~1000 switch (cmd) { case 0x01: // 前进 // 左右轮都正转 HAL_GPIO_WritePin(GPIOA, AIN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, AIN2_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, BIN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, BIN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, ccr); break; case 0x02: // 后退 // 正反逻辑反过来 HAL_GPIO_WritePin(GPIOA, AIN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, AIN2_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, BIN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, BIN2_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, ccr); break; case 0x03: // 左转左轮停右轮正转 HAL_GPIO_WritePin(GPIOA, AIN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, AIN2_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, BIN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, BIN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, ccr); break; case 0x04: // 右转右轮停左轮正转 HAL_GPIO_WritePin(GPIOA, AIN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, AIN2_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, BIN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, BIN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 0); break; case 0x05: // 停止 HAL_GPIO_WritePin(GPIOA, AIN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, AIN2_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, BIN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, BIN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 0); break; } }转向方式我没做“左前轮转右前轮转”的那种阿克曼转向而是两轮差速。左转时左轮不转、右轮正转小车就会原地向左拐在室内地板上非常灵活转弯半径几乎为零。如果想让转弯更柔可以把case 0x03里停下来的那侧改成低速转动而不是完全停。差速比例的调校上我个人经验是先让速度那一侧保持和前进一致然后从0开始往上加停侧的速度找一个既不打滑又能顺畅画弧的临界点。4.3 上位机测试手机发一个字节轮子动一下硬件烧录完之后建议按下面顺序做联调别一上来就发控制指令大概率出问题不知道是哪个环节先用USB转TTL连接STM32的USART1调试串口打开串口助手。手机连上8266热点打开网络调试助手TCP连接192.168.4.1:8080。发送AA 01 50 FB 55前进速度80。看USART1打印的rx_cmd和rx_speed是不是0x01和0x50。如果是说明ESP8266 → STM32链路正常小车轮子应该动起来。如果轮子不动先检查PWM是否启动、驱动芯片的方向引脚电平是否正常。这里特别注意校验和的计算。手机网络调试助手一般是按HEX发送你自己得先计算好校验和再填进去。我的习惯是在电脑上用Python写个几行的小脚本自动把指令转成十六进制帧这样每次发不同速度和方向时不用手算校验和def make_frame(cmd, speed): b0 0xAA b4 0x55 b3 (b0 cmd speed) 0xFF return bytes([b0, cmd, speed, b3, b4]) print(make_frame(0x01, 0x50).hex().upper()) # 输出 AA0150FB555. 实测记录粘包、掉电、电磁干扰三座大山5.1 粘包没有想象中可怕状态机全吞了我第一次用手机连续疯狂点击“前进”按钮8266串口偶尔会一次性吐出来十来二十个字节。第一反应是“坏了协议包全粘一起了”。但仔细看数据其实TCP粘包只是把多帧首尾相连只要每帧内部字节顺序不错状态机就可以一帧一帧切干净。实测下来接收状态机非常稳。但如果上位机手机APP发送太快导致TCP重传出现字节顺序错乱情况就复杂了。这时候校验和能兜底错误帧直接被丢弃下一帧继续正常。代码里状态机在帧尾校验失败时会回到WAIT_HEAD不会卡死。5.2 ESP8266一启动电机就重启99%是供电问题这是我在实际跑车时最崩溃的坑。现象是小车静止时8266连得好好的手机控制完全正常但只要电机一转WiFi就断开8266开始反复重启。排查过程用万用表量8266的VCC发现电机启动瞬间电压从3.3V跌到2.6V以下。原因就是STM32板上那颗3.3V LDO稳压芯片最大输出电流也就几百毫安电机启动电流轻松吃掉大半留给8266的电流不够WiFi发射瞬间电压跌落模块就复位了。解决方案也不复杂把8266的电源从STM32板上独立出来用一个5V转3.3V的AMS1117稳压模块单独给它供电同时和STM32共地。另外在8266的VCC和GND之间并一个100uF的电解电容能吸收电机启动时的瞬时压降。改完供电之后这个问题再没出现过。5.3 前进变左转、指令乱码电磁干扰与布线教训有一次调完车发现前进指令偶尔会变成左转。帧协议和校验都正常但就是会出现“合法但错误”的指令。这种问题比乱码还难排查因为校验和通过了数据是完整的但内容不对。后来用示波器观察USART2的RX引脚发现电机PWM线一靠近8266的天线区域RX线上就会叠加毛刺串口电平偶尔被干扰翻转一个bit。0x01变成0x03正好是前进变左转。处理办法有三条我全用了把8266模组挪到小车尾部远离电机驱动板天线方向朝上不要和PWM线平行走线。串口线PA2、PA3到8266的TX、RX用双绞线降低共模干扰。临时把波特率从115200降到9600。波特率低意味着位宽更大同样时长的毛刺造成误码的概率大幅下降。8266侧执行ATUART_DEF9600,8,1,0,0STM32侧CubeMX里也改成9600两边保持一致。代价是传输速率降低但每帧才5个字节影响可以忽略。5.4 失控保护没有指令就自动停车实测中还有一个体验问题如果手机APP被杀掉或者玩家误触了WiFi断开TCP连接直接断开正常也就意味着不会再收到数据命令了。但此时小车如果正在全速前进……一个没有司机的小车在全速前进这是安全隐患。我的做法很简单在定时器中断里维护一个1秒的看门狗计数器。每次主循环解析到有效指令就清零如果1秒内没有收到任何有效指令直接调用car_control(0x05, 0)让小车停车。这类“超时保护”对遥控车来说不是可选项是必选项。无线链路再稳定也有断的时候越是跑得快的车越要有这个兜底逻辑。最后关于调车顺序的一点私人体会写完代码别急着把车放地上跑我交过学费车一加速就冲出去撞墙心疼坏了。最好是把小车架空四个轮子悬空先验证所有指令动作是否正确、转向方向是否和预期一致再放到地上小范围慢慢试。另外手机网络调试助手APP在退出WiFi时不会主动通知8266断开重新连上后需要手动点一下“重连”不然指令发不出去这个不是代码问题别花时间在代码里折腾。如果这第4篇你从头跟下来了接下来最大的可玩性我觉得是做一个网页遥控界面用ESP8266的Web Server功能直接在浏览器里画一个虚拟方向盘按钮手机扫码就能控车彻底告别网络调试助手那朴素的界面。祝你的小车跑得稳。本文还有配套的精品资源点击获取