1. 项目缘起为什么选择这套方案大家好我是老张一个在嵌入式圈子里摸爬滚打了十来年的“老油条”。这些年从单片机到ARM再到各种五花八门的AIoT开发板我基本都折腾过。最近我发现身边不少刚入门的朋友尤其是学生和转行做嵌入式的兄弟总想找一个成本低、效果好、能跑通的实战项目来练手。他们常常被复杂的网络协议、交叉编译和环境配置劝退最后板子只能吃灰。这不前段时间我在淘宝上瞎逛偶然发现了Luckfox Pico这块小板子价格才四十来块还自带一个完整的Linux系统RISC-V架构的这性价比简直了配套的摄像头模组也就五十出头。我当时就想这不正是给新手朋友们量身定做的“练手神器”吗用一百块的成本就能搭建一个属于自己的实时视频监控或者远程查看系统这学习动力不就来了所以我决定写这篇手把手的教程带大家从零开始用Luckfox Pico和Ubuntu主机搭建一个基于UDP协议的实时视频流传输系统。整个流程我会把我踩过的坑、调试的小技巧都揉碎了讲清楚。你不用有太深的网络编程或者OpenCV基础跟着我的步骤一步步来保证你能看到摄像头画面从开发板“飞”到你的电脑屏幕上。我们的目标很简单不搞理论只搞实操让你亲手把系统跑起来获得最直接的成就感。2. 开箱与准备你的硬件和软件清单工欲善其事必先利其器。在动手写代码之前咱们得先把“家伙事儿”备齐了。别担心东西不多也不贵。2.1 硬件三件套Luckfox Pico开发板这是我们的核心。它本质上是一个集成了RV1103芯片的微型计算机运行着完整的Buildroot Linux系统。你拿到手的就是一块比信用卡还小的绿色板子上面有Type-C供电口、TF卡槽、摄像头接口等。它的算力足够处理摄像头采集、图像编码和网络发送这些任务。Luckfox Pico配套摄像头官方有配套的摄像头模组直接插在板子的FPC排线座上就行。我用的就是这款省去了驱动兼容的麻烦。确保你买的是兼容型号通常是OV系列传感器分辨率支持到720P甚至1080P。一张TF卡Micro SD卡这是开发板的“硬盘”。官方系统镜像需要烧录到卡里。容量不用太大8GB或16GB完全够用Class 10及以上速度的卡会更流畅。我手头正好有张闲置的16GB卡就它了。2.2 软件全家桶一台安装Ubuntu的电脑或虚拟机这是我们的接收端和显示端。我强烈建议直接在物理机上安装Ubuntu 20.04或22.04 LTS版本稳定性最好。如果你只有Windows电脑那么安装VMware或VirtualBox虚拟机也是完全可行的本篇教程就是基于虚拟机环境演示的。虚拟机设置会稍微多一步网络配置后面我会详细说。MobaXtermWindows用户必备这是一个功能强大的终端软件。因为Luckfox Pico通过USB连接电脑后会虚拟出一个网络设备我们可以通过SSH登录到板子的Linux系统进行操作。MobaXterm集成了SSH客户端、SFTP文件传输等功能比用系统自带的命令行方便太多。Luckfox官方Wiki和工具这是最重要的参考资料。你需要去Luckfox的官网Wiki页面找到系统镜像烧录工具RKDevTool和详细的入门指南。把系统镜像下载好并按照教程烧录到TF卡中。这一步是基础务必确保板子能正常启动并登录。一个重要的前期准备我强烈建议你先完全按照Luckfox官方的“opencv-mobile”示例教程操作一遍。那个教程会教你如何在板子上编译并运行一个最简单的OpenCV程序从摄像头捕获一张照片并保存。这一步能帮你确认几个关键问题摄像头驱动是否正常、OpenCV环境是否就绪、开发板与你的电脑网络是否连通。把这步跑通后面我们自己的项目就成功了一大半相当于打通了“任督二脉”。3. 网络环境搭建让板子和电脑“对上话”这是整个项目第一个容易卡住的地方。我们需要让Luckfox Pico以下简称板子、你的Windows宿主机、以及Ubuntu虚拟机三者处于同一个网络段并且能互相访问。别被吓到跟着我一步步设置其实很简单。3.1 Windows与板子的连接板子通过Type-C线连接到Windows电脑后电脑会识别出一个新的网络适配器名字通常叫“Remote NDIS based Internet Sharing Device”。这个虚拟网卡就是你和板子通信的桥梁。打开Windows的“网络和共享中心” - “更改适配器设置”找到这个新出现的网络连接。右键点击它选择“属性”然后双击“Internet协议版本4 (TCP/IPv4)”。选择“使用下面的IP地址”IP地址输入172.32.0.100这个地址只要和板子在同一网段且不冲突就行子网掩码输入255.255.255.0网关和DNS可以不填。点击确定保存。现在打开MobaXterm点击“Session” - “SSH”。在“Remote host”里输入板子的IP地址172.32.0.93这是板子出厂默认的静态IP。用户名填root密码填luckfox。点击OK如果一切顺利你就会看到一个Linux命令行提示符恭喜你已经成功登录到你的Luckfox Pico了3.2 配置Ubuntu虚拟机的网络为了让Ubuntu也能和板子对话我们需要将虚拟机的网络模式设置为“桥接”并且桥接到我们刚才在Windows里设置的那个虚拟网卡上。关闭你的Ubuntu虚拟机。打开VMware或VirtualBox的虚拟网络编辑器通常在“编辑”菜单下。这是一个高级设置。在虚拟网络编辑器里找到“桥接模式”相关的设置。关键一步来了在“桥接到”的下拉菜单中选择你刚才在Windows里看到的那个“Remote NDIS...”网络适配器。这一步是告诉虚拟机直接使用这个物理网卡进行通信。应用设置。打开虚拟机的设置找到“网络”选项将网络连接方式设置为“桥接模式”并确认桥接的网卡是刚才选择的那个。启动Ubuntu虚拟机。进入Ubuntu系统后我们需要手动设置一个静态IP让它和板子、Windows在同一网段。打开终端编辑网络配置文件。对于Ubuntu 20.04/22.04通常使用netplan。你可以先运行ip addr查看你的网卡名称比如ens33或enp0s3。编辑配置文件sudo nano /etc/netplan/01-netcfg.yaml文件名可能略有不同。在文件中配置类似如下内容请替换ens33为你的实际网卡名network: version: 2 ethernets: ens33: addresses: [172.32.0.101/24] # gateway4: 172.32.0.1 # 本例不需要网关可注释掉 nameservers: addresses: [8.8.8.8, 114.114.114.114]保存退出CtrlX 按Y 回车。应用配置sudo netplan apply。现在你可以在Ubuntu终端里ping 172.32.0.93试试看能不能通。如果能看到来自板子的回复那么恭喜你最磨人但最关键的网络环境已经搭建成功了Windows、Ubuntu、Luckfox Pico三方已经可以自由通信。4. 开发板程序详解采集、编码与发送好了网络通了我们可以开始写代码了。首先搞定发送端也就是Luckfox Pico上的程序。它的任务很明确打开摄像头抓取一帧图像压缩成JPG格式然后通过UDP协议发送给Ubuntu。4.1 代码结构与核心逻辑我基于官方OpenCV示例修改了一个完整的发送程序。你可以在板子上用vi或nano编辑器创建一个新文件比如叫udp_sender.cpp。下面我分段解释关键部分。头文件与网络配置这部分引入了必要的库并定义了接收端的地址和端口。SERVER_IP必须填你Ubuntu虚拟机的IP我们刚才设的172.32.0.101SERVER_PORT可以自定义一个比如8888只要两边一致就行。#include opencv2/core/core.hpp #include opencv2/highgui/highgui.hpp #include opencv2/imgproc/imgproc.hpp #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include string.h #include unistd.h #define SERVER_IP 172.32.0.101 // 你的Ubuntu虚拟机IP #define SERVER_PORT 8888 // 自定义端口号初始化摄像头这里我们使用OpenCV的VideoCapture类。注意Luckfox Pico官方提供的OpenCV是精简版本opencv-mobile功能完整但体积小。它支持摄像头采集和基本的图像处理但可能不支持读取磁盘图片文件所以官方示例里读图片的代码被注释掉了。我们设置采集分辨率为320x240这是一个兼顾速度和清晰度的尺寸非常适合网络传输。cv::VideoCapture cap; cap.set(cv::CAP_PROP_FRAME_WIDTH, 320); cap.set(cv::CAP_PROP_FRAME_HEIGHT, 240); if (!cap.open(0)) { // 参数0通常代表第一个摄像头 fprintf(stderr, 无法打开摄像头\n); return -1; } // 短暂等待摄像头稳定 sleep(1);创建UDP Socket这是网络编程的基础。我们创建一个AF_INETIPv4的SOCK_DGRAM数据报即UDP类型的socket。如果创建失败程序会报错退出。int sockfd socket(AF_INET, SOCK_DGRAM, 0); if (sockfd 0) { perror(创建Socket失败); exit(EXIT_FAILURE); } // 设置目标服务器地址结构 struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(SERVER_PORT); // htons将主机字节序转为网络字节序 inet_pton(AF_INET, SERVER_IP, server_addr.sin_addr); // 转换IP地址核心循环采集、编码、发送为了让视频“动”起来我们需要一个无限循环。在循环里不断从摄像头抓取帧压缩然后发送。这里有一个非常重要的技巧UDP协议本身不保证数据包的顺序和完整性一个大图片直接发可能被拆成多个包容易乱序或丢失。因此我们采用一个简单的协议先发送一个4字节的整数图像数据长度再发送图像数据本身。这样接收方就知道该接收多少数据。while (true) { cv::Mat frame; cap frame; // 捕获一帧 if (frame.empty()) { fprintf(stderr, 捕获到空帧跳过。\n); continue; } // 将图像编码为JPG格式到内存缓冲区 std::vectoruchar buf; std::vectorint params {cv::IMWRITE_JPEG_QUALITY, 80}; // 设置JPG质量80是个不错的平衡点 if (!cv::imencode(.jpg, frame, buf, params)) { fprintf(stderr, 图像编码失败\n); continue; } // 首先发送图像数据的大小4字节 uint32_t img_size buf.size(); uint32_t net_size htonl(img_size); // 转为网络字节序 sendto(sockfd, net_size, sizeof(net_size), 0, (struct sockaddr*)server_addr, sizeof(server_addr)); // 然后发送图像数据本身 // 注意实际发送可能需要分片这里简单处理假设一次能发完 int bytes_sent sendto(sockfd, buf.data(), buf.size(), 0, (struct sockaddr*)server_addr, sizeof(server_addr)); if (bytes_sent 0) { perror(发送图像数据失败); } else { fprintf(stderr, 发送了一帧大小%u 字节\n, img_size); } // 控制一下帧率不要太快把板子跑满或者网络堵死 usleep(50 * 1000); // 休眠50毫秒大约20FPS } cap.release(); close(sockfd);4.2 在板子上编译与运行将完整的代码保存后我们需要在Luckfox Pico上编译它。Luckfox Pico官方系统已经配置好了交叉编译工具链非常方便。确保你已经通过MobaXterm登录到板子。进入你的代码所在目录。使用g编译需要链接OpenCV库。命令如下g udp_sender.cpp -o udp_sender pkg-config --cflags --libs opencv4 -stdc11如果pkg-config找不到opencv4可以尝试opencv。或者直接指定库路径通常是这样g udp_sender.cpp -o udp_sender -I/usr/include/opencv4 -lopencv_core -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -stdc11编译成功后会生成一个叫udp_sender的可执行文件。先别急着运行因为接收端还没准备好。5. Ubuntu接收端程序接收、解码与显示现在我们来搞定接收端。在Ubuntu虚拟机上我们将编写一个程序监听指定的UDP端口接收开发板发来的数据还原成图像并显示出来。5.1 搭建OpenCV开发环境首先确保你的Ubuntu已经安装了OpenCV。如果没有安装非常简单。打开终端执行sudo apt update sudo apt install libopencv-dev python3-opencv安装完成后可以写个简单的测试程序验证一下。创建一个test_opencv.cpp文件#include opencv2/opencv.hpp #include iostream int main() { std::cout OpenCV版本: CV_VERSION std::endl; cv::Mat img(200, 300, CV_8UC3, cv::Scalar(0,255,0)); // 创建一个绿色图片 cv::imshow(测试, img); cv::waitKey(0); return 0; }用g test_opencv.cpp -o testpkg-config --cflags --libs opencv4 编译并运行如果弹出一个绿色窗口说明环境OK。5.2 接收程序核心代码解析接收程序的核心逻辑与发送端对应创建UDP Socket、绑定端口、循环接收。关键点在于如何正确解析我们自定义的简单协议。创建Socket与绑定我们需要创建一个UDP Socket并将其“绑定”到我们之前约定的端口如8888上。绑定意味着这个Socket专门监听这个端口的数据。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include opencv2/opencv.hpp #define PORT 8888 #define BUFFER_SIZE (1024 * 64) // 缓冲区设大点容纳一帧JPG数据 int main() { int sockfd; struct sockaddr_in server_addr, client_addr; socklen_t client_len sizeof(client_addr); char buffer[BUFFER_SIZE]; // 创建Socket sockfd socket(AF_INET, SOCK_DGRAM, 0); if (sockfd 0) { perror(socket创建失败); exit(EXIT_FAILURE); } // 配置并绑定地址 memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有网络接口 server_addr.sin_port htons(PORT); if (bind(sockfd, (const struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(绑定端口失败); close(sockfd); exit(EXIT_FAILURE); } printf(UDP服务器已启动正在监听端口 %d ...\n, PORT);协议解析与数据接收接下来是核心循环。我们首先接收4个字节这应该是发送端发来的图像长度信息。由于网络字节序和主机字节序可能不同我们需要用ntohl函数转换回来。然后根据这个长度去接收实际的图像数据。while (true) { uint32_t net_img_size 0; uint32_t img_size 0; // 第一步接收4字节的长度信息 int recv_len recvfrom(sockfd, net_img_size, sizeof(net_img_size), 0, (struct sockaddr*)client_addr, client_len); if (recv_len ! sizeof(net_img_size)) { fprintf(stderr, 接收长度信息错误。\n); continue; } img_size ntohl(net_img_size); // 转换为主机字节序 // 简单的数据校验防止错乱的数据包导致崩溃 if (img_size 0 || img_size BUFFER_SIZE) { fprintf(stderr, 收到无效的图像大小: %u\n, img_size); continue; } // 第二步接收图像数据本身 recv_len recvfrom(sockfd, buffer, img_size, 0, (struct sockaddr*)client_addr, client_len); if (recv_len ! img_size) { fprintf(stderr, 接收图像数据不完整期望 %u实际收到 %d\n, img_size, recv_len); continue; } printf(成功接收一帧大小%u 字节\n, img_size);解码与显示图像收到完整的JPG数据后我们使用OpenCV的imdecode函数直接从内存缓冲区解码图像。如果解码成功就显示出来。// 从内存缓冲区解码JPG图像 cv::Mat raw_data(1, recv_len, CV_8UC1, buffer); // 将buffer包装成OpenCV的Mat cv::Mat frame cv::imdecode(raw_data, cv::IMREAD_COLOR); if (!frame.empty()) { cv::imshow(Luckfox Pico 实时视频流, frame); // waitKey的参数控制显示刷新和退出1表示等待1毫秒这样不会阻塞 if (cv::waitKey(1) 27) { // 按ESC键退出 printf(用户终止程序。\n); break; } } else { fprintf(stderr, 图像解码失败\n); } } close(sockfd); cv::destroyAllWindows(); return 0; }5.3 编译与运行接收端在Ubuntu上使用类似的命令编译这个接收程序假设文件名为udp_receiver.cppg udp_receiver.cpp -o udp_receiver pkg-config --cflags --libs opencv4 -stdc11编译成功后先运行接收端程序./udp_receiver你会看到终端打印出“UDP服务器已启动正在监听端口 8888 ...”程序开始等待数据。6. 联调测试与效果优化激动人心的时刻到了现在回到你的MobaXterm在Luckfox Pico的终端里运行我们之前编译好的发送程序./udp_sender如果一切配置正确几秒钟内你应该能在Ubuntu的桌面上看到一个名为“Luckfox Pico 实时视频流”的窗口里面显示着从开发板摄像头传来的实时画面虽然可能有点延迟画面也可能偶尔卡顿但这标志着你的第一个跨平台实时视频流系统已经成功跑通了实测中可能遇到的问题与优化画面卡顿或延迟大这通常是网络或处理速度的问题。可以尝试降低分辨率或帧率在发送端将摄像头分辨率从320x240降到160x120或者增加usleep的时间比如到100毫秒即10FPS。调整JPG压缩质量代码中cv::IMWRITE_JPEG_QUALITY参数默认是95可以降低到70或60能显著减少数据量牺牲一点画质换取流畅度。检查网络确保没有其他程序占用大量带宽。在虚拟机设置中可以尝试将网络适配器类型设置为“性能更好”的选项如VMware的VMXNET3。接收端收不到数据或数据错乱防火墙检查Ubuntu的防火墙是否屏蔽了8888端口。可以临时关闭防火墙测试sudo ufw disable测试完记得开启sudo ufw enable。IP地址错误再三确认发送端代码中的SERVER_IP是否填对了Ubuntu的IP172.32.0.101。协议错位这是新手最容易出错的地方。确保发送端是先发htonl(图像大小)的4个字节再发图像数据。接收端是先收4个字节用ntohl转换再按这个大小收数据。一个字节都不能错。想实现真正的“流”而不是单张图片我们现在的程序是“抓一帧-发一帧”的循环已经实现了最基本的流。如果你想更稳定可以考虑引入多线程一个线程专门抓取摄像头帧放入队列另一个线程专门从队列取帧压缩发送这样可以避免采集和发送相互等待。第一次看到自己电脑上显示出另一个设备摄像头捕捉的画面那种感觉是非常奇妙的。它把抽象的“网络编程”、“图像处理”概念变成了一个看得见摸得着的成果。这个项目虽然基础但它是一个完整的端到端系统涵盖了嵌入式开发、Linux操作、网络通信、图像处理多个知识点。你可以在此基础上继续扩展比如加入运动检测、人脸识别Luckfox Pico的RV1103带NPU可以跑轻量模型或者把UDP换成TCP保证可靠性甚至写一个简单的手机APP来接收显示。