1. 为什么智能车调试要上WiFi图传先想清楚再动手1.1 没有图传时调车到底痛在哪这几年带队的经历让我有一个很深的感触智能车能不能跑得快一半看调参另一半看你怎么“看得见”车上的数据。我们在赛道边最常干的事就是把车拎回来、插上串口线、打开逐飞虚拟示波器然后对着波形和图像一帧一帧地翻。这个流程刚入门的时候还能忍等车真正跑起来速度一上去痛点就全出来了车跑完一圈摄像头图像到底是入弯时丢线了还是出弯时补线补歪了只能靠车上的SD卡回放但很多同学根本没接SD卡想实时看灰度图像串口线不够长车一出去线就得拔下来拔下来以后就成了“盲调”车撞了、翻了一圈首要的就是确认摄像头是否还正常可人跑过去之前摄像头到底是什么状态你永远不知道。WiFi图传解决的就是这个“最后一米”的问题。把主控板采集到的原始图像数据通过无线发回电脑端你站在赛道外面就能像看监控一样实时盯着摄像头画面。这也算是一种远程调试能帮你把调车效率提上来一个台阶。1.2 常见的三种图传方案该怎么选我先给新手朋友们排个雷给智能车做图传市面上能走的路线其实就那么几条但能用在“逐飞主控板 逐飞摄像头”组合上的方案并不是所有都合适。方案A直接买现成的WiFi图传模块比如某些支持WiFi输出的摄像头模块。这类模块很多自带MCU和摄像头直接接电池供电然后用手机或电脑看画面。优点是非常省事缺点也很明显它跟主控板的图像数据完全无关你看的是“另一个摄像头”的画面而不是主控板真正在用的那路图像。调车时如果图像处理逻辑出问题这种图传根本帮不上忙。方案B用逐飞官方/山外那套无线调试产品。这属于“官方路线”有配套上位机稳定性好但需要额外买硬件而且有些协议细节是封装好的想自己改一改图像压缩方式、分辨率、封装格式时会觉得受限。方案C自己移植用主控板现有的总钻风/OV7725摄像头把采集到的图像通过串口发给ESP8266/ESP32再由ESP8266以WiFi透明传输方式发到电脑。这个方案实现成本低、代码完全可控而且能真正看到主控板正在处理的图像。缺点是需要自己写一点协议和上位机但也就是一两天工作量的事。我下面要展开的就是方案C。它的移植思路和代码基本可以平移到逐飞的RT1064、TC264、TC377这些常见主控板上只要串口和摄像头接口对应得上就行。1.3 移植之前先明确这套方案的数据流向很多同学一拿到代码就急着编译下载结果连信号从哪到哪都没搞清楚出了问题根本无从下手。我先用一句话把整套流程串起来摄像头总钻风 → 主控板逐飞核心板采集一帧灰度图 → 主控板内部做尺寸缩小和帧格式封装 → UART串口 → ESP8266WiFi透明传输 → 电脑端的TCP服务程序 → 上位机显示画面。注意这里ESP8266扮演的是“串口转WiFi的桥”它本身不处理图像只负责把串口收到的原始字节流原封不动地发到电脑。所以只要电脑端能收到一个稳定字节流剩下的工作全在主控板和上位机两边。这种做法的好处是图像处理逻辑完全不受WiFi影响你可以直接在电脑上看到总钻风采集到的每一帧原始灰度图像然后判断线上的二值化阈值、丢线判断逻辑是否靠谱。2. 硬件准备与接线别让一根线毁掉一次调车2.1 物料清单先把要用的东西摆出来基本就下面这些物料型号/说明数量主控板逐飞RT1064核心板/TC264等只要带UART和摄像头接口就行1摄像头逐飞总钻风灰度摄像头OV7725188×120灰度输出1WiFi模块ESP8266-01S或NodeMCU开发板推荐用NodeMCU方便供电和调试1串口调试助手电脑端用于先单独调试ESP82661杜邦线/排针若干面包板可选方便接线我建议新手直接用NodeMCU版本的ESP8266因为上面自带了USB转串口芯片和稳压电路不用外接电平转换也方便先用电脑把AT指令调通。等整套流程验证完再考虑换更小的ESP-01嵌入车模。2.2 摄像头到主控板的连接逐飞总钻风摄像头用的是软排线接口直接插到核心板对应的摄像头接口上就行。注意插的时候金色触点朝下插到位以后能听到“咔哒”一声。这里容易出现一个低级失误排线插反或者只插进去一半导致初始化失败图像全黑或者花屏。插好之后在主控板代码里确认摄像头初始化对应的接口。RT1064上用的通常是DVP接口加DMA逐飞SDK里camera_init()会自动把引脚配置好不需要自己连一根根信号线。2.3 主控板到ESP8266的UART接线与电平匹配这块要仔细看。ESP8266的串口电平是3.3V逐飞主控板的串口引脚电平也是3.3V所以正常情况下可以直接连。但如果你用的是5V供电的NodeMCU开发板引脚电平还是3.3V依然没问题。接线对应关系如下主控板UART引脚ESP8266引脚说明TXRX主控板发送ESP8266接收RXTXESP8266发送主控板接收GNDGND必须共地否则数据全是乱的5V/3.3VVCC推荐外接稳定5V给开发板见2.4我自己踩过一个大坑只接了TX、RX忘记共地结果串口打印全部是乱码纠结了好久才反应过来。串口通信本质上是看两个设备之间的电平差地都不通电平差就是漂移的数据不可能对。2.4 电源分配建议ESP8266在WiFi发射瞬间的电流尖峰可以达到300mA以上如果直接从主控板3.3V引脚取电很容易把主控板电压拉低导致主控板复位。这个“WiFi一发射主控板就重启”的问题非常经典。我的建议是如果用的是NodeMCU开发板直接给它接一个独立的5V电源比如电池经过稳压模块出来的5V让它自己板载稳压到3.3V如果用的是裸ESP-01模块供电走3.3V但一定要在模块电源脚旁边并联一个100uF~470uF的电解电容和0.1uF的陶瓷电容滤掉瞬时压降主控板和ESP8266之间串口电平转换一般不需要但如果发现ESP8266收不到数据优先检查VCC电压是否稳定而不是怀疑引脚接错。3. 图像数据量估算与协议设计为什么直接发整帧会卡死3.1 总钻风图像的原始数据量总钻风摄像头输出的是灰度图分辨率是188×120每个像素用1字节表示灰度值0~255。那么$$188 \times 120 22560 \text{ 字节/帧}$$如果完全不压缩、不缩小直接通过串口发一整帧我们来看看需要多大的带宽。3.2 串口波特率下的理论帧率串口发送是按字节算的波特率921600时加上起始位和停止位每秒实际最多传约92160字节。那么理论帧率是$$92160 / 22560 \approx 4.1 \text{ 帧/秒}$$这个帧率实际上只能算“幻灯片级别”而且还要算上采集图像、打包协议、WiFi转发丢包重传等开销实际能到2帧/秒就不错了。如果是115200波特率那理论帧率只有0.5帧/秒基本不可用。所以我建议先做两件事把图像缩小每两列取一列、每两行取一行得到94×60的灰度图数据量降到 $94 \times 60 5640$ 字节/帧把波特率拉高直接设置到921600保证瓶颈不在串口上。缩小之后的帧率估算如下表波特率有效数据速率94×60灰度图理论帧率实际可参考帧率115200约11520 B/s2.0 帧/秒1~2 帧/秒460800约46080 B/s8.2 帧/秒5~7 帧/秒921600约92160 B/s16.3 帧/秒10~14 帧/秒实际调车时10帧左右的灰度图已经足够看清赛道元素和丢线情况了。如果你后面想进一步提高帧率可以把图像灰度量化为4bit甚至二值化数据量还能再砍一半到四分之一。3.3 帧协议设计帧头长度数据校验无线传输不像有线串口那么稳定TCP还好一些如果以后改用UDP就会丢包。所以我们必须给数据设计一个简单的帧格式让电脑端能在一堆字节流中认出“一帧图像从哪里开始、到哪里结束”。我用的是一个非常经典的协议格式字节位置内容说明00xAA帧头110x55帧头22高度缩小后的图像高度比如603宽度缩小后的图像宽度比如944 ~ 4W×H-1图像数据灰度像素按行排列末尾校验和前面所有字节累加取低8位帧头用0xAA 0x55是为了让接收端能快速找到起点。宽度和高度字段一定不能省否则接收端不知道一帧到底有多大。校验和用最原始的累加和虽然不如CRC结实但对于调试阶段完全够用而且算起来快。我见过不少同学一上来就整CRC32把单片机端代码写得很复杂结果采集一帧的时间还没发出去的时间长。调试阶段的协议够用就好等稳定了再升级。4. 主控板端代码移植逐飞SDK基础上一步步改4.1 摄像头初始化把官方例程的取图逻辑先跑通在动手改WiFi相关代码之前先确保你的摄像头单独跑官方例程能出图。我不知道你用的具体是哪款逐飞核心板但流程基本一致用逐飞官方例程创建一个工程确认摄像头能正常初始化在循环里调用采集函数把图像数据打印到LCD或虚拟示波器上看一眼确认图像是完整的188×120灰度图没有花屏、割裂、偏色。RT1064上的初始化代码看起来就像这样#include headfile.h #define IMAGE_W 188 #define IMAGE_H 120 static uint8 image_data[IMAGE_H][IMAGE_W]; int main(void) { clock_init(SYSTEM_CLOCK_600M); debug_init(); camera_init(); // 初始化总钻风摄像头 while (1) { if (camera_get_frame(image_data[0][0], IMAGE_W, IMAGE_H) 0) { continue; } // 到这里image_data 里就是一帧完整的灰度图像 } }需要说明的是camera_get_frame的具体返回值和参数在不同版本的逐飞SDK里可能略有差别有的版本是直接在回调里置一个结束标志位。不管用哪种方式核心逻辑都一样等摄像头DMA把一帧搬运完再去处理图像不要在采集到一半时去读写图像缓冲区。这里有个新手容易忽略的问题我们缩小的图像发送最好直接用原始图像缓冲区在内存里原地抽取而不是先拷贝一份再缩小。原因很简单逐飞主控板带摄像头DMA时通常会使用双缓冲缓冲区可能同时被摄像头硬件写入。你如果不小心把正在被DMA写入的区域拿去读就会出现“上半帧是新的下半帧是旧的”这种割裂画面。4.2 图像数据缩放从188×120降到适合无线传输的尺寸我采用的是最简单的等间隔抽点也就是每两行取一行、每两列取一列。虽然这会丢失一些细节但总钻风的灰度图本身信息量足够抽点后94×60依然能清晰看到赛道边缘、斑马线和十字路口。下面这个函数的作用就是把原始188×120图像抽成94×60输出到发送缓冲里#define SEND_W 94 #define SEND_H 60 static uint8 send_buf[4 SEND_W * SEND_H 1]; static uint16_t pack_frame(uint8 *out, uint8 *img, uint16_t img_w, uint16_t img_h) { uint16_t index 0; uint16_t i, j; uint8_t sum 0; // 帧头、高度、宽度 out[index] 0xAA; out[index] 0x55; out[index] SEND_H; out[index] SEND_W; // 等间隔抽点 for (i 0; i img_h; i 2) { for (j 0; j img_w; j 2) { out[index] img[i * img_w j]; } } // 累加和校验 for (i 0; i index; i) { sum out[i]; } out[index] sum; return index; }这里要注意抽点时img_w和img_h必须是偶数否则循环越界。总钻风是188×120本来就是偶数所以没问题。如果你以后换了别的摄像头最好在函数开头加断言。想保留更多图像细节的话也可以改成隔两行取两行、隔两列取两列数据量会翻一倍帧率相应下降自己权衡。4.3 串口初始化与发送选好UART口注意波特率上下限主控板端最关键的配置就是串口。以RT1064为例一般用UART4或者UART5作为调试串口具体引脚要查逐飞的引脚分配表。我习惯把WiFi图传单独放一个UART不要和调试打印共用免得图像数据把调试信息冲掉。初始化代码#define WIFI_UART UART_4 // 根据自己板子和SDK选择 #define WIFI_BAUD 921600 void wifi_uart_init(void) { uart_init(WIFI_UART, WIFI_BAUD); }发送一帧时直接把打包好的send_buf发出去uint16_t frame_len pack_frame(send_buf, image_data[0][0], IMAGE_W, IMAGE_H); uart_putbuff(WIFI_UART, send_buf, frame_len);有些逐飞SDK版本里串口发送函数的名字可能不一样比如uart_write_buffer没关系换成你SDK里对应的接口就行。只要最终效果是“把一整块buffer的字节按顺序发出去”就对了。我建议不要在主循环里用uart_putchar一个一个字节地发因为每个字符函数调用都有开销921600波特率下可能中间出现断流。尽量用SDK提供的批量发送接口一次性把整帧丢给串口硬件DMA。4.4 主循环整合采集、打包、发送的状态机整合之后的主循环比大家想象的要简单。逐飞摄像头一般用DMA连续采集所以主循环只需要检查“这一帧是否已经准备好了”然后打包发送即可while (1) { if (camera_get_frame(image_data[0][0], IMAGE_W, IMAGE_H) 0) { continue; } uint16_t frame_len pack_frame(send_buf, image_data[0][0], IMAGE_W, IMAGE_H); uart_putbuff(WIFI_UART, send_buf, frame_len); }别小看这个循环它已经是一个完整的“采集-发送”状态机了。真正做到后面如果帧率不够就要开始排查什么地方耗时最长。我实测下来90%的耗时都花在uart_putbuff等待串口发送完成上因为921600波特率下5640字节一帧发完需要大约60ms。这个时间没办法完全消除除非你改成DMA发送让CPU在DMA搬运数据的同时去跑图像处理算法。5. ESP8266配置与串口透传把AT指令玩明白5.1 先用电脑单独调通ESP8266别一上来就连主控板很多同学栽在ESP8266上原因是把ESP8266接到主控板上之后串口助手就连不上了。所以我强烈建议先用USB转串口模块把ESP8266单独插到电脑上用串口助手发AT指令验证一遍。NodeMCU开发板本身带USB转串口插上电脑打开设备管理器确认COM口号波特率设成115200然后输入AT如果返回OK说明模块正常。ESP8266默认波特率是115200。因为我们要跟主控板跑921600所以需要用AT指令把波特率改掉ATUART_DEF921600,8,1,0,0改完之后串口助手的波特率也要同步改成921600再发AT验证。这一步很容易忘了同步电脑端波特率导致误判。5.2 配置WiFi连接与TCP Client接下来把ESP8266设置为Station模式连接你的路由器或者手机热点ATCWMODE1 ATCWJAP你的WiFi名称,你的WiFi密码如果连接成功会返回WIFI CONNECTED然后OK。这里有几点经验手机热点名称里尽量不要有中文和特殊字符ESP8266对SSID编码支持一般手机热点建议选2.4GHz频段ESP8266不支持5GHz电脑和ESP8266必须连同一个WiFi不然TCP连不上。然后配置TCP客户端连接电脑上运行的Python上位机ATCIPMUX0 ATCIPSTARTTCP,192.168.1.100,8080这里192.168.1.100换成你电脑的IP地址。查看电脑IP的方法很简单Windows下在命令行输入ipconfig找到无线网卡对应的IPv4地址Linux下用ifconfig或ip addr。5.3 透传模式和单片机对接TCP连接成功之后输入下面的指令进入透传模式ATCIPMODE1 ATCIPSEND进入透传模式后ESP8266会返回一个提示符之后所有从串口进来的数据都会被原封不动转发到TCP服务器。也就是说主控板往串口发什么电脑端就能收到什么。这里有一个关键点透传模式一旦开启AT指令就失效了你需要发送“”退出透传模式才能重新发AT指令。所以调试时建议先在串口助手验证完整流程再接到主控板上。接上主控板之前记得把ESP8266的串口TX/RX和主控板的UART交叉连接然后重新上电。主控板端开机后ESP8266需要几秒时间去连接WiFi并建立TCP在此之前主控板发过来的数据会被丢掉。所以我的建议是主控板上电后延时2~3秒再开始发图像或者在上电时判断一下ESP8266是否已经进入透传状态。6. PC端接收与显示写一个最简Python上位机6.1 环境准备Python OpenCV电脑端我用Python写了一个最简上位机只需要安装两个库pip install opencv-python numpy不依赖逐飞官方上位机也不依赖任何商业软件代码就一百来行。运行起来后脚本会开启一个TCP服务端ESP8266连上来之后就能直接开始收图像。6.2 TCP服务端接收与组帧接收端的难点在于图像数据是一个接一个的字节流可能在中间断开也可能一帧数据分好几次到达。所以必须先收进缓冲区再从缓冲区里按协议把完整帧找出来。下面这版代码我已经在RT1064总钻风的组合上跑过可以直接用import socket import cv2 import numpy as np HOST 0.0.0.0 # 监听所有网卡 PORT 8080 SEND_W 94 SEND_H 60 server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, PORT)) server.listen(1) print([等待] ESP8266 连接...) conn, addr server.accept() print([已连接], addr) buf b frame_len 2 2 SEND_W * SEND_H 1 # 帧头2 高宽2 数据 校验1 while True: data conn.recv(4096) if not data: print([断开] 连接已断开) break buf data index 0 while len(buf) - index frame_len: if (buf[index] 0xAA and buf[index 1] 0x55 and buf[index 2] SEND_H and buf[index 3] SEND_W): packet buf[index:index frame_len] # 校验和验证 s 0 for b in packet[:-1]: s (s b) 0xFF if s packet[-1]: # 提取图像数据并显示 img np.frombuffer(packet[4:4 SEND_W * SEND_H], dtypenp.uint8) img img.reshape(SEND_H, SEND_W) img_scale cv2.resize(img, (SEND_W * 4, SEND_H * 4), interpolationcv2.INTER_NEAREST) cv2.imshow(WiFi Image, img_scale) if cv2.waitKey(1) 0xFF 27: # 按ESC退出 break index frame_len continue index 1 # 丢弃已经处理完和无法匹配的数据 buf buf[index:] conn.close() server.close() cv2.destroyAllWindows()这段代码最关键的地方是内层的while循环它一遍遍扫描缓冲区找到一个合法的帧头就尝试按帧长度截取完整帧校验和通过就显示不通过就往右移动一个字节继续找。这样即使一帧数据中间被WiFi的传输延迟切断了也能在下一轮循环里把剩余部分拼回来。显示的时候因为94×60太小我用cv2.resize把它放大4倍显示方便看细节。放大用INTER_NEAREST保持像素边缘锐利不会把图像搞糊。6.3 实时显示与FPS统计把代码跑起来以后如果一切正常你会看到一个灰度窗口显示主控板摄像头看到的画面。想确认帧率的话可以临时加一个计数器frame_count 0 last_time time.time() # 在每次显示一帧后 frame_count 1 if frame_count % 30 0: now time.time() fps frame_count / (now - last_time) print(fFPS: {fps:.2f}) last_time now frame_count 0注意电脑端要关掉防火墙对Python进程的拦截否则ESP8266可能连不上TCP端口。Windows弹防火墙提示时一定要选择“允许访问”。7. 实测效果与排错心得从“出图像”到“稳定图传”7.1 实测参数参考我在逐飞RT1064核心板 总钻风摄像头上实测使用921600波特率、94×60灰度图、NodeMCU作为WiFi桥距离10米以内、无遮挡时帧率稳定在10~13帧/秒图像完整无撕裂。如果换成TCP协议偶尔会出现一帧被拆成多段到达的情况但组帧逻辑能正常处理画面不会花。如果距离超过15米或者中间有金属车架遮挡WiFi信号会衰减此时帧率会掉到6~8帧/秒偶尔还会出现连续几帧丢失。我的建议是调车时把电脑放在赛道附近手机热点也放近一点ESP8266的天线尽量避开碳纤维车架。7.2 常见问题一为什么单片机端发了一帧就卡死这个坑我印象太深了。第一次接通时图像只显示了一帧然后主控板就直接死机。排查了半天最后发现是send_buf的长度定义不够我把数组定义成了uint8 send_buf[4 SEND_W * SEND_H]但实际帧尾还有一个校验和数组越界了。数组越界在单片机上是玄学问题有时候不报错只在特定数据量下把其他变量踩坏。解决办法是把发送缓冲大小留足甚至在末尾多留几个字节static uint8 send_buf[4 SEND_W * SEND_H 8];另外还有可能是串口发送函数自身的问题。有些逐飞SDK的uart_putbuff接口如果传入的长度是0或者被中断打断会卡在等待状态。遇到这种情况可以在发送前关一下中断发完再开但这个需要谨慎影响实时性。7.3 常见问题二为什么ESP8266连不上热点我总结了几个高频原因电脑和ESP8266不在同一个网段。比如电脑连的是路由器但ESP8266连的是手机热点那肯定连不上TCP热点频率是5GHzESP8266不支持。手机热点要手动设置成2.4GHz优先热点开启了AP隔离或客户端隔离设备之间无法互访。有些公共WiFi就这样换个路由器热点就好TCP端口被防火墙拦截。先临时关闭防火墙测试能通再考虑加白名单。调试时不要凭感觉猜用串口助手先看ESP8266返回的信息。ATCWJAP失败会有明确的error code连接TCP失败也会有提示一步一步排查其实很快。7.4 常见问题三图像花屏、缺行、马赛克这类问题绝大多数不是WiFi丢包而是主控板端发送的数据本身就不连续。我遇到过一次“每隔几行就花一次屏”的情况排查后发现是串口发送用了两个函数调用先发帧头和数据的一部分再发剩余部分。中间被摄像头DMA中断打断两个串口发送操作中间插入了一大段其他数据接收端就乱了。解决办法是把整帧所有数据一次性放到一个连续内存里然后一次性调用批量发送接口不要在发送过程中被其他任务打断。如果必须分段发送就在发送前enter_critical禁止中断发完再退出。还有一个可能电源波动导致ESP8266内部丢数据。这种情况通常表现为“WiFi连接正常但电脑端偶尔收到乱码”。解决方法是给ESP8266单独供电并加一个大电容。7.5 几个提高传输效率的小技巧等整套图传稳定之后你肯定不满足于10帧这里分享几个我实际验证过的小技巧把灰度图变成二值图再发送。智能车图像处理里二值化后的图像才是算法真正看的东西直接发送二值图不仅帧率翻倍而且调试时能看到阈值调节的真实效果用DMA发送。逐飞SDK支持串口DMA发送把发送缓冲地址交给DMA后CPU可以同时去跑图像处理帧率能再提升30%左右局部裁剪。很多时候你只关心赛道前方60行那就只发图像顶部120行里的60行数据量直接减半把ESP8266的TCP连接拆成UDP。UDP少了ACK确认和重传延迟更低但会有丢包风险需要在组帧协议上加更多容错。8. 写在最后给新手的建议整套移植做下来老实说难度并不高真正的门槛在于你愿不愿意一步一步把串口、WiFi、TCP、组帧这四个环节单独调通再合到一起。我见过太多同学一上来就把所有代码堆在一起结果出了问题根本不知道是摄像头没出图、串口没发送、还是ESP8266没连上。我的建议是严格按这个顺序走先在串口助手里调通ESP8266的AT指令再用电脑串口助手直接看主控板发的帧头能不能收到最后才把ESP8266串进去。每一步都验证通过再合起来跑整个过程会顺利很多。个人体会是WiFi图传这东西一旦跑通了一次后面换主控板、换摄像头、换压缩方案都非常快因为核心的移植思路是一致的先搞定摄像头出图再搞定数据传输最后搞定PC端显示。你掌握的不只是“给逐飞主控板加图传”而是一套可以复用的无线图像调试方案。
