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

1615

积分

1

好友

227

主题
发表于 3 天前 | 查看: 7| 回复: 0

在实现高速、多通道数据采集时,FPGA方案因其并行处理能力和确定性时延成为主流选择。实施该方案的首要步骤是明确通讯协议,即定义FPGA与SoC之间SPI接口的完整帧格式,包括起始位、数据位和校验位。其次,需要正确配置SoC端的SPI DMA控制器。在 Linux 内核中,需确保SPI控制器的DMA功能已启用,可通过检查 /sys/kernel/debug/dmaengine/summary 文件来确认。

硬件时间戳:消除系统抖动的关键

在精密采集系统中,硬件时间戳是消除Linux系统调度抖动、中断延迟以及SPI传输不确定性的核心方案。其核心思路是在FPGA内部维护一个由GPS的PPS信号校准的高精度计时器,并在ADC采样的瞬间“锁存”当前计数值,将该时间戳与采样数据一同打包发送给SoC。

FPGA需要实现一个两级结构的计时器:

  • 秒计数器:记录从某个基准时间开始的绝对秒数。
  • 纳秒计数器:由FPGA晶振驱动,记录当前秒内的偏移量(例如,使用100MHz晶振,则步长为10ns)。

FPGA内部工作流程

  1. PPS同步:当GPS的PPS信号上升沿到达时,FPGA将“纳秒计数器”清零,并将“秒计数器”加1。
  2. 采样锁存:在ADC的采样开始信号或数据就绪信号触发的瞬间,FPGA将当前的秒和纳秒计数值锁存到专用的寄存器中。
  3. 数据打包:FPGA将多通道的ADC数据与锁存的8字节时间戳拼接成一个完整的数据帧。例如,对于6通道的32位ADC,数据部分为24字节,加上时间戳后构成32字节的长帧。

数据帧格式定义

为便于Linux驱动解析,建议采用如下固定长度的帧格式(以6通道32位ADC为例),共32字节:

偏移 (Byte) 长度 内容描述 备注
0 - 3 4 通道 1 数据 32-bit Signed Integer
4 - 23 20 通道 2 - 6 数据 依次排列
24 - 27 4 UTC秒数 FPGA 内部秒计数器
28 - 31 4 纳秒偏移 FPGA 内部纳秒计数器

Linux驱动实现示例

以下是一个基于IIO框架的驱动模板,它利用触发缓冲区来处理来自FPGA的中断,并读取包含硬件时间戳的完整数据帧。

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/iio/iio.h>
#include <linux/iio/buffer.h>
#include <linux/iio/trigger_helper.h>
#include <linux/iio/triggered_buffer.h>

/* FPGA 拼接帧定义: 6通道 * 4字节(32位) = 24字节 */
#define FPGA_ADC_CHAN_COUNT    6
#define FPGA_ADC_DATA_SIZE     (FPGA_ADC_CHAN_COUNT * 4)
/* 缓冲大小需考虑对齐,通常为:数据长度 + 填充 + 8字节时间戳 */
#define FPGA_ADC_SCAN_SIZE     (FPGA_ADC_DATA_SIZE + 8)

struct fpga_adc_state {
    struct spi_device *spi;
    struct mutex lock;
    /* 保证 DMA 传输安全的对齐缓冲区 */
    u8 rx_buf[FPGA_ADC_SCAN_SIZE] ____cacheline_aligned;
};

/* IIO 通道配置 */
#define FPGA_ADC_CHAN(idx) {                         \
    .type = IIO_VOLTAGE,                             \
    .indexed = 1,                                    \
    .channel = idx,                                  \
    .scan_index = idx,                               \
    .scan_type = {                                   \
        .sign = 's',                                 \
        .realbits = 32,                              \
        .storagebits = 32,                           \
        .endianness = IIO_BE,                        \
    },                                               \
}

static const struct iio_chan_spec fpga_adc_channels[] = {
    FPGA_ADC_CHAN(0),
    FPGA_ADC_CHAN(1),
    FPGA_ADC_CHAN(2),
    FPGA_ADC_CHAN(3),
    FPGA_ADC_CHAN(4),
    FPGA_ADC_CHAN(5),
    /* 硬件时间戳通道:8 字节 (4s + 4ns) */
    {
        .type = IIO_TIMESTAMP,
        .channel = -1,
        .scan_index = 6,
        .scan_type = {
            .sign = 'u',
            .realbits = 64,
            .storagebits = 64,
            .endianness = IIO_BE,
        },
    },
};

/**
 * 触发处理函数:当 FPGA 的 DRDY 中断触发时执行
 */
static irqreturn_t fpga_adc_trigger_handler(int irq, void *p)
{
    struct iio_poll_func *pf = p;
    struct iio_dev *indio_dev = pf->indio_dev;
    struct fpga_adc_state *st = iio_priv(indio_dev);

    /* 一次性读取 32 字节 (24字节数据 + 8字节硬件时间戳) */
    spi_read(st->spi, st->rx_buf, 32);

    /* 注意:这里使用 iio_push_to_buffers 而不是 _with_timestamp */
    /* 因为时间戳已经是 rx_buf 数据的一部分了 */
    iio_push_to_buffers(indio_dev, st->rx_buf);
    iio_trigger_notify_done(indio_dev->trig);

    return IRQ_HANDLED;
}

static int fpga_adc_probe(struct spi_device *spi)
{
    struct iio_dev *indio_dev;
    struct fpga_adc_state *st;
    int ret;

    /* 1. 分配 IIO 设备内存 */
    indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*st));
    if (!indio_dev)
        return -ENOMEM;

    st = iio_priv(indio_dev);
    st->spi = spi;
    mutex_init(&st->lock);

    indio_dev->name = "fpga_mag_collector";
    indio_dev->info = NULL; // 可以在此添加 read_raw 处理单次读取
    indio_dev->modes = INDIO_DIRECT_MODE;
    indio_dev->channels = fpga_adc_channels;
    indio_dev->num_channels = ARRAY_SIZE(fpga_adc_channels);

    /* 2. 设置触发缓冲区 (Triggered Buffer) */
    ret = devm_iio_triggered_buffer_setup(&spi->dev, indio_dev,
                        NULL, // top half (不需要)
                        fpga_adc_trigger_handler, // bottom half (核心)
                        NULL);
    if (ret)
        return ret;

    /* 3. 注册 IIO 设备 */
    return devm_iio_device_register(&spi->dev, indio_dev);
}

static const struct of_device_id fpga_adc_dt_ids[] = {
    { .compatible = "my_org,fpga-adc-aggregator" },
    { }
};
MODULE_DEVICE_TABLE(of, fpga_adc_dt_ids);

static struct spi_driver fpga_adc_driver = {
    .driver = {
        .name = "fpga_adc",
        .of_match_table = fpga_adc_dt_ids,
    },
    .probe = fpga_adc_probe,
};
module_spi_driver(fpga_adc_driver);

MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("FPGA aggregated 6-channel ADC driver");
MODULE_LICENSE("GPL v2");

设备树配置

在设备树文件中添加如下配置,以确保 SPI 的DMA和中断能正常工作。

&spi0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi0m1_pins>; // 根据实际引脚组选择

    /* 核心:开启 DMA 以支持高速 32 字节连读 */
    dmas = <&dmac0 0>, <&dmac0 1>;
    dma-names = "tx", "rx";

    adc_collector@0 {
        compatible = "my_org,fpga-adc-aggregator";
        reg = <0>;
        spi-max-frequency = <25000000>; // 建议 20-50MHz
        /* FPGA 端的 DRDY 中断引脚 */
        interrupt-parent = <&gpio3>;
        interrupts = <RK_PC2 IRQ_TYPE_EDGE_FALLING>;
        status = "okay";
    };
};

时钟漂移问题与校准方案

FPGA晶振存在固有误差,例如标称100MHz的晶振实际可能是100.001MHz,这将导致时间戳产生累积误差。解决方法包括:

  1. FPGA动态补偿:在PPS信号到来时,读取纳秒计数器的值。如果该值大于理论值(例如1,000,050,000 ns),说明晶振偏快,FPGA可在下一秒内通过微调内部时钟分频系数来补偿这额外的50,000ns。
  2. 应用层线性插值:记录每一秒内接收到的总采样点数N。假设某一秒的起始绝对时间为T_start_sec,则该秒内第n个采样点的精确时间戳T_n可通过公式T_n = T_start_sec + n / N计算得出。这种方法在FPGA端未做动态补偿时尤为有效,能显著提升时间戳的长期一致性。



上一篇:2026年React组件库选型指南:主流评测、性能对比与避坑指南
下一篇:开源网页3D演示工具Immersa:GLB模型导入与动画场景编辑指南
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2025-12-24 17:18 , Processed in 0.173744 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

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