在基于“SoC + FPGA + ADC”的混合架构中,当FPGA完成了数据拼帧(24字节ADC数据 + 8字节硬件时间戳)后,SoC应用层需要高效地将这些数据取出并还原为物理意义上的“UTC时间 + 信号”。使用 libiio 库是最推荐的方式,它屏蔽了底层的 mmap 和字符设备操作,极大地简化了驱动开发工作。
从应用层的视角看,每一帧数据是一个32字节的数据块:
- 0-23字节:6路32-bit ADC的原始采样数据。
- 24-31字节:8字节硬件时间戳(由FPGA产生,高4字节为秒,低4字节为纳秒)。
要开始解析数据,首先需要在SoC上安装 libiio-dev 开发库。
应用层libiio数据采集代码解析
下面的C语言示例演示了如何使用libiio库从名为“fpga_mag_collector”的IIO设备中循环读取并解析数据。
#include <iio.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <signal.h>
#include <inttypes.h>
static bool stop = false;
void sig_handler(int s) { stop = true; }
int main(int argc, char **argv) {
struct iio_context *ctx;
struct iio_device *dev;
struct iio_buffer *buf;
struct iio_channel *chans[6], *ts_chan;
signal(SIGINT, sig_handler);
// 1. 创建本地上下文(适用于RK3568等直接在设备上运行的情况)
ctx = iio_create_local_context();
if (!ctx) {
perror("No iio context found");
return -1;
}
// 2. 查找设备(名字需与Linux驱动中定义的 .name 一致)
dev = iio_context_find_device(ctx, "fpga_mag_collector");
if (!dev) {
fprintf(stderr, "Device not found\n");
iio_context_destroy(ctx);
return -1;
}
// 3. 启用6个电压采集通道
for (int i = 0; i < 6; i++) {
// 根据实际情况查找通道,例如按ID或名称
chans[i] = iio_device_find_channel(dev, "voltage0", false);
// 或者使用: chans[i] = iio_device_get_channel(dev, i);
iio_channel_enable(chans[i]);
}
// 4. 启用硬件时间戳通道
ts_chan = iio_device_find_channel(dev, NULL, true); // 查找timestamp类型通道
if (ts_chan) iio_channel_enable(ts_chan);
// 5. 创建缓冲区(设置为一次读取1024帧数据)
buf = iio_device_create_buffer(dev, 1024, false);
if (!buf) {
perror("Could not create buffer");
iio_context_destroy(ctx);
return -1;
}
printf("Starting data acquisition...\n");
while (!stop) {
// 从内核缓冲区获取一批新数据
ssize_t ret = iio_buffer_refill(buf);
if (ret < 0) break;
// 获取单帧数据的步长(总字节数,此处应为32)
ptrdiff_t step = iio_buffer_step(buf);
void *p_dat, *p_end;
// 遍历缓冲区中的每一帧
p_end = iio_buffer_end(buf);
for (p_dat = iio_buffer_first(buf, chans[0]); p_dat < p_end; p_dat += step) {
// --- 提取6通道ADC数据 ---
int32_t adc_val[6];
for (int i = 0; i < 6; i++) {
// 定位到当前帧内对应通道数据的指针
int32_t *v_ptr = (int32_t*)(p_dat + i * 4);
adc_val[i] = *v_ptr;
// 注意:若驱动设置了IIO_BE标志,libiio可能已处理字节序。
// 若数值异常,可使用 be32toh(*v_ptr) 手动进行大端转主机序。
}
// --- 提取8字节硬件时间戳(位于帧偏移24字节处)---
uint64_t raw_hw_ts = *(uint64_t*)(p_dat + 24);
// 按FPGA定义解析:高32位为秒,低32位为纳秒
uint32_t utc_sec = (uint32_t)(raw_hw_ts >> 32);
uint32_t utc_nsec = (uint32_t)(raw_hw_ts & 0xFFFFFFFF);
// 打印结果(实际应用中可写入HDF5或文件)
printf("Time: %u.%09u s | Ch0: %d | Ch5: %d\n",
utc_sec, utc_nsec, adc_val[0], adc_val[5]);
}
}
iio_buffer_destroy(buf);
iio_context_destroy(ctx);
return 0;
}
时序同步的核心思想
上述代码成功实现了数据读取,但其核心价值在于硬件时间戳带来的精确时序同步。在FPGA逻辑中,当ADC的DRDY(数据就绪)信号有效的瞬间,FPGA内部的utc_sec和utc_nsec计数器数值就被锁存到寄存器中,并与该帧ADC数据绑定。无论后续的SPI传输耗时1ms还是10ms,也无论Linux内核调度产生多大延迟,时间戳记录的始终是采样发生的绝对时刻。
这意味着,如果你有两台部署在不同地点的SoC进行同步采集,只要各自的FPGA计时器均通过GPS的PPS(脉冲每秒)信号校准过,那么从两者获取的utc_sec.utc_nsec数据就可以直接用于跨设备的精确时间对齐与互相关计算,这是实现分布式测控系统的关键。
架构深度剖析:FPGA作为智能数据枢纽
在六通道高速ADC采集场景下,由于单片ADC输出的数据位宽较大(32位数据+状态位),若直接用SoC同时驱动6个SPI从机,极易导致时序混乱并给CPU带来巨大负载。因此,FPGA在此架构中扮演了“智能数据集散中心”的角色,它在ADC采样完成的瞬间,将空间维度(6通道信号)与时间维度(硬件时间戳)的数据“冻结”并打包。
典型的数据流转路径如下:
- 并行采样 (Parallel Latch):FPGA同时驱动所有ADC的
CONVST(转换开始)引脚,启动同步采样。
- 并行移位 (Parallel Shift):FPGA启动6个独立的SPI Master模块,同时从6片ADC读取数据。
- 时间戳锁存 (Timestamp Capture):在
DRDY有效的精确时刻,锁存全局的UTC_Seconds和Nanoseconds计数器值。
- 写入 FIFO (FIFO Push):将拼接好的32字节(256位)数据帧推入一个异步FIFO缓冲区。
为了增强数据在传输过程中的可靠性,建议在每帧数据的开头加入一个同步字,以便于在Linux端检测数据流是否发生错位。
| 一个简化的帧结构定义如下: |
偏移 |
大小 |
内容 |
说明 |
| 0x00 |
2 Byte |
0xEB90 |
同步帧头 (Magic Number) |
| 0x02 |
2 Byte |
Status |
FPGA状态位(如增益、溢出标志等) |
| 0x04 |
24 Byte |
Data |
6通道32-bit原始电压数据 |
| 0x1C |
4 Byte |
UTC_Sec |
硬件秒计数器 |
| 0x20 |
4 Byte |
UTC_nSec |
硬件纳秒计数器 |
关键实现细节与稳定性设计
SoC作为SPI Master,其时钟是间歇性的(需要时才发起读取),而ADC采样是连续进行的。协调这两者间的时序是系统稳定的核心。
- 使用异步 FIFO:FPGA内部必须设计一个双时钟域FIFO。写时钟由ADC采样频率决定;读时钟则由SoC的SPI时钟驱动。这就像一个数据蓄水池,即使SoC因内核调度延迟了数百微秒才来读取,采样数据仍安全缓存在FIFO中,不会丢失。
- 帧对齐检测:SPI传输中最怕丢失一个比特,导致后续所有数据错位。FPGA端应在SPI片选信号
CS为高时重置其从机状态机。SoC应用层在读取每帧数据后,应校验前2字节是否为0xEB90。若校验失败,应立即丢弃当前缓冲区并尝试重新初始化连接,这是确保网络通信可靠性的常见思路。
- 信号质量与时序约束:在20MHz至50MHz的SPI速率下,需仔细考虑SoC与FPGA间的PCB走线。必须满足建立时间和保持时间的要求。通常,FPGA应在SCLK的下降沿更新数据,确保SoC在上升沿能采样到稳定电平。长距离传输时,建议在SPI线上串联匹配电阻以减少信号反射。
- FPGA拼帧逻辑框架:
// 简化的拼帧控制状态机
always @(posedge clk_sys) begin
case (state)
IDLE: begin
if (adc_drdy_all) begin
// 1. 锁存时间戳到帧缓冲区高位
frame_buf[255:224] <= current_utc_sec;
frame_buf[223:192] <= current_utc_ns;
state <= READ_ADC;
end
end
READ_ADC: begin
// 2. 启动6个SPI模块并行读取,完成后得到adc_data_all (192 bits)
if (read_done) begin
frame_buf[191:0] <= adc_data_all;
state <= PUSH_FIFO;
end
end
PUSH_FIFO: begin
// 3. 将256bit (32字节) 帧写入异步FIFO,并添加帧头
fifo_wr_en <= 1;
fifo_din <= {16'hEB90, 16'h0000, frame_buf};
state <= IDLE;
end
endcase
end
- 其他注意事项:
- 位序问题:ADC输出通常为大端序。FPGA拼帧需保持此序。SoC(如ARM64)为小端序,在libiio解析时需使用
be32toh()进行转换。
- 溢出处理:需定义当SoC读取过慢导致FIFO满时的策略(如丢弃最旧数据或停止写入),并通过状态位通知SoC。
- 空闲状态:当SPI读取时FIFO为空,FPGA应发送特定空闲码或全零,避免MISO引脚高阻态导致SoC读到随机噪声,这也是嵌入式系统设计中保证数据确定性的常用方法。