找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2270

积分

0

好友

320

主题
发表于 2025-12-24 17:33:39 | 查看: 39| 回复: 0

在基于“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_secutc_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通道信号)与时间维度(硬件时间戳)的数据“冻结”并打包。

典型的数据流转路径如下:

  1. 并行采样 (Parallel Latch):FPGA同时驱动所有ADC的CONVST(转换开始)引脚,启动同步采样。
  2. 并行移位 (Parallel Shift):FPGA启动6个独立的SPI Master模块,同时从6片ADC读取数据。
  3. 时间戳锁存 (Timestamp Capture):在DRDY有效的精确时刻,锁存全局的UTC_SecondsNanoseconds计数器值。
  4. 写入 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采样是连续进行的。协调这两者间的时序是系统稳定的核心。

  1. 使用异步 FIFO:FPGA内部必须设计一个双时钟域FIFO。写时钟由ADC采样频率决定;读时钟则由SoC的SPI时钟驱动。这就像一个数据蓄水池,即使SoC因内核调度延迟了数百微秒才来读取,采样数据仍安全缓存在FIFO中,不会丢失。
  2. 帧对齐检测:SPI传输中最怕丢失一个比特,导致后续所有数据错位。FPGA端应在SPI片选信号CS为高时重置其从机状态机。SoC应用层在读取每帧数据后,应校验前2字节是否为0xEB90。若校验失败,应立即丢弃当前缓冲区并尝试重新初始化连接,这是确保网络通信可靠性的常见思路。
  3. 信号质量与时序约束:在20MHz至50MHz的SPI速率下,需仔细考虑SoC与FPGA间的PCB走线。必须满足建立时间和保持时间的要求。通常,FPGA应在SCLK的下降沿更新数据,确保SoC在上升沿能采样到稳定电平。长距离传输时,建议在SPI线上串联匹配电阻以减少信号反射。
  4. 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
  5. 其他注意事项
    • 位序问题:ADC输出通常为大端序。FPGA拼帧需保持此序。SoC(如ARM64)为小端序,在libiio解析时需使用be32toh()进行转换。
    • 溢出处理:需定义当SoC读取过慢导致FIFO满时的策略(如丢弃最旧数据或停止写入),并通过状态位通知SoC。
    • 空闲状态:当SPI读取时FIFO为空,FPGA应发送特定空闲码或全零,避免MISO引脚高阻态导致SoC读到随机噪声,这也是嵌入式系统设计中保证数据确定性的常用方法。



上一篇:CSS3核心特性全解析:现代Web界面构建、响应式布局与动画实战指南
下一篇:SpringBoot打包方式选择:JAR与WAR的性能差异与优化实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 09:33 , Processed in 0.299278 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表