在嵌入式系统开发中,实现高速、稳定的数据采集与传输是一个常见且具有挑战性的任务。尤其在需要处理多通道、高采样率信号的场景下,对 SoC 的数据处理能力和通信链路都提出了较高要求。本文将以瑞芯微 RK3568 平台为例,探讨如何设计一个从 FPGA 通过 USB 高速采集数据,并在 Linux 端进行缓冲、存储,最终通过 WiFi 实时传输至 Windows 主机的完整方案。
首先,我们来算一笔“数据账”,明确系统需要处理的负载。假设需求为多通道(例如12通道)、1MHz 采样率、持续 10-20 秒。数据速率大致为:通道数 × 1MHz × 2字节/样本 (假设为16位或32位ADC) = 24 MB/s。总数据量则达到:24 MB/s × 20s = 480 MB。这个量级的数据对存储的实时性和 网络(尤其是 WiFi)链路的稳定性带来了考验。
SoC 端的数据接收与存储方案
RK3568 需要建立一条高效的数据链路来接收并保存来自 USB 的数据。
1. 数据接收 (USB)
- 驱动层:通常,FPGA 会被配置为模拟一个 USB Bulk 传输设备。在 Linux 应用层,我们可以直接使用
libusb 库来读取 Bulk 端点的数据,这种方式绕过了内核的文件抽象,延迟更低,效率更高。
- 双缓存机制:为了防止存储或网络发送线程的短暂阻塞导致 USB 数据丢包,必须在内存中开辟环形缓冲区。这是一种典型的生产者-消费者模型:
- 线程 A(生产者):专门负责通过 USB 持续读取数据,并写入环形缓冲区。
- 线程 B(消费者):负责从环形缓冲区取出数据,进行本地存储或网络发送。
2. 数据存储
- 存储位置:
- RAM(临时):如果数据仅用于后续通过网络发送,可以将其临时存储在挂载为
tmpfs 内存文件系统的 /tmp 目录下。这样做读写速度极快,并且避免了频繁写入对 eMMC 等存储设备寿命的损耗。
- eMMC/NVMe(永久):如果需要持久化保存采集的原始数据,应直接写入二进制文件(如
.bin 或 .dat 格式)。
- 存储格式:至关重要的一点是,不要在高速采集过程中进行任何复杂的协议封装(如转换成 JSON 或 CSV 文本格式)。应直接存储原始二进制流,这能最大限度地减少 CPU 开销,并保持最小的文件体积。
通过 WiFi 发送数据到 Windows
前面计算出的 24 MB/s 的持续带宽对普通 WiFi 是一个考验。为确保稳定,强烈建议使用 5GHz 频段(802.11ac 或 ax 协议),2.4GHz 频段通常难以维持如此高的稳定速率。
实时流传输 (Socket 编程)
这是实现高效传输的首选方法。
- 协议选择:使用 TCP。尽管 UDP 的传输开销更小、速度理论上更快,但它不保证数据包的顺序和可靠性。对于 ADC 采集的二进制流,丢失任何一个字节都可能导致后续所有数据解析错位,这是灾难性的。TCP 的可靠传输特性完美契合此需求。
- 实现逻辑:
- Windows 端:运行一个 TCP Server(可以使用 Python、C# 或 C语言 等轻松实现),监听特定端口,等待连接并接收数据。
- RK3568 端:作为 TCP Client,在数据采集完成后(或另一个线程在采集的同时),主动向 Windows 服务器发起连接,并调用
send() 函数发送文件数据。
使用 C 语言进行 Socket 发送时,核心目标是高效率和稳定性。考虑到数据量约 480MB,应采用分块发送的方式,而非一次性读入内存。以下是一个示例代码,展示了如何分块读取文件并通过 TCP 发送:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SERVER_IP "192.168.1.100" // Windows 端的 IP 地址
#define SERVER_PORT 8888 // 与 Windows 端一致的端口
#define CHUNK_SIZE 65536 // 每次发送 64KB,提高 WiFi 吞吐效率
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("用法: %s <数据文件路径>\n", argv[0]);
return -1;
}
const char *filename = argv[1];
int sock = 0;
struct sockaddr_in serv_addr;
char *buffer = malloc(CHUNK_SIZE);
// 1. 打开数据文件
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
perror("无法打开文件");
return -1;
}
// 2. 创建 Socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket 创建失败 \n");
return -1;
}
// 设置发送缓冲区大小(可选,优化 WiFi 性能)
int sndbuf_size = 1024 * 1024; // 1MB
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sndbuf_size, sizeof(sndbuf_size));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
// 将 IP 地址从字符串转换为二进制格式
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
printf("\n 无效的地址/ 地址不支持 \n");
return -1;
}
// 3. 连接 Windows 服务端
printf("正在连接到 Windows 服务端 %s:%d...\n", SERVER_IP, SERVER_PORT);
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\n 连接失败 \n");
return -1;
}
// 4. 开始分块发送数据
printf("开始发送数据: %s\n", filename);
size_t n;
long total_sent = 0;
while ((n = fread(buffer, 1, CHUNK_SIZE, fp)) > 0) {
ssize_t sent = send(sock, buffer, n, 0);
if (sent < 0) {
perror("发送失败");
break;
}
total_sent += sent;
// 打印进度(每 10MB 打印一次)
if (total_sent % (10 * 1024 * 1024) < CHUNK_SIZE) {
printf("已发送: %.2f MB\n", (double)total_sent / (1024 * 1024));
}
}
printf("发送完成,总计发送: %.2f MB\n", (double)total_sent / (1024 * 1024));
// 5. 收尾工作
fclose(fp);
free(buffer);
close(sock);
return 0;
}
这段代码的关键点在于:
- 分块读取与发送:使用固定大小的缓冲区(如64KB)循环读取文件并发送,避免内存耗尽。
- 设置 Socket 缓冲区:适当调大发送缓冲区(SO_SNDBUF)有助于平滑 WiFi 传输中的波动,提升整体吞吐量。
- 进度反馈:在发送过程中打印进度,便于监控。
总结
整个高速采集与传输 系统设计 的核心在于“分而治之”和“缓冲解耦”。通过 USB Bulk 传输、环形缓冲区、原始二进制存储和 TCP 分块流式传输的组合,可以在 RK3568 这类嵌入式平台上有效地处理数十 MB/s 级别的数据流。实际部署时,还需重点关注 WiFi 信号强度、网络干扰以及服务器端的接收处理能力,确保端到端的稳定性。
如果你对嵌入式开发、高速数据链路或网络编程有更多兴趣,欢迎到 云栈社区 与更多开发者交流探讨,获取相关的项目源码和深入的技术资料。