在实现高速、多通道数据采集时,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内部工作流程
- PPS同步:当GPS的PPS信号上升沿到达时,FPGA将“纳秒计数器”清零,并将“秒计数器”加1。
- 采样锁存:在ADC的采样开始信号或数据就绪信号触发的瞬间,FPGA将当前的秒和纳秒计数值锁存到专用的寄存器中。
- 数据打包: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,这将导致时间戳产生累积误差。解决方法包括:
- FPGA动态补偿:在PPS信号到来时,读取纳秒计数器的值。如果该值大于理论值(例如1,000,050,000 ns),说明晶振偏快,FPGA可在下一秒内通过微调内部时钟分频系数来补偿这额外的50,000ns。
- 应用层线性插值:记录每一秒内接收到的总采样点数N。假设某一秒的起始绝对时间为
T_start_sec,则该秒内第n个采样点的精确时间戳T_n可通过公式T_n = T_start_sec + n / N计算得出。这种方法在FPGA端未做动态补偿时尤为有效,能显著提升时间戳的长期一致性。