news 2026/9/17 5:49:54

Intel SoC FPGA中断驱动开发:从设备树到Linux驱动全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Intel SoC FPGA中断驱动开发:从设备树到Linux驱动全流程

相信做 Intel SoC FPGA 的朋友都有这种体会:FPGA 里辛辛苦苦做了个中断源,结果 HPS 这边的 Linux 一点反应都没有,要么设备树不对,要么驱动没写好。作为系列教程的第四篇,这篇文章就聚焦“设计中断驱动程序”这一环。前几篇我们已经把 FPGA 端的 IP、中断信号的接法、以及设备树里中断属性的描述搞定了,这一篇的核心目标很简单:让 Linux 跑起来之后,FPGA 一拉中断,HPS 端能够正确触发、进入我们写的处理函数,并且数据通路不出幺蛾子。适合已经能完成硬件工程和基础设备树配置、正准备踏入 Linux 内核驱动这块的开发者参考。

我直接用项目中一个比较典型的场景来讲:FPGA 侧有一个数据采集逻辑,数据准备好后就拉高一根中断线,HPS 端需要及时读取这批数据并做处理。整个链路涉及 GIC 中断控制器、设备树中断映射、platform driver 的 probe 流程、以及中断处理函数的上半部和下半部。代码说多不多,但每个环节都有深坑。这一篇我尽量把从零到能跑、能验证的步骤全部铺开,附带平时不怎么写进文档的排查经验。

1 整体思路:先搞清楚中断从 FPGA 到驱动函数的完整通路

动手写驱动前,我强烈建议先把“中断是怎么一步步捅到我们代码里”的路径捋一遍。否则一旦出错,你根本不知道是设备树写歪了、驱动注册没成功、还是中断号就不对。

1.1 硬件侧的中断链路

Agilex 5 系列 SoC FPGA 的 HPS 端集成的是 ARM GIC(Generic Interrupt Controller),FPGA 侧的中断信号经过配置后,会作为 SPI(Shared Peripheral Interrupt)或者 PPI(Peripheral Private Interrupt)送到 GIC。实际工程里绝大多数是 SPI。

从 FPGA fabric 拉出来的中断信号,一般要连到 HPS 的一个专用中断输入引脚,然后通过设备树里指定的中断号映射到 Linux 的中断子系统。这个中断号在 Agilex 5 的参考手册里有明确描述,但我们一般不硬编码,而是从设备树里读。

这里先建立一个最关键的概念:设备树里填的中断号,和 Linux 内核里驱动拿到的 IRQ number 并不一定是同一个数字。设备树通过interrupts = <GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH>这种属性来描述硬件中断源,内核里的 irqchip 驱动会把硬件中断号转换成 Linux 内部使用的 global IRQ number。驱动里用platform_get_irq()拿到的是后者,可以直接传给request_irq()

1.2 Linux 中断处理的上半部和下半部

中断处理函数是跑在中断上下文里的,要求快进快出。所以驱动一般会分两部分:

  • 上半部(hardirq handler):只做最必要的事,比如读状态寄存器、清中断标志、把数据搬运到缓冲区,然后返回IRQ_WAKE_THREADIRQ_HANDLED
  • 下半部(threaded IRQ / tasklet / workqueue):做耗时操作,比如数据解析、唤醒用户态进程、通过 netlink 上报事件等。

对新手来说,我比较推荐直接用request_threaded_irq()配合线程化中断。它把下半部封装成一个内核线程,开发难度低,不用太担心睡眠问题,而且调试起来很直观。如果你的延时要求极端严格,再考虑 tasklet 或硬中断里直接处理。

1.3 为什么平台驱动最合适

这一系列都是围绕 SoC FPGA 的 HPS 开发,设备树已经把外设描述好了,所以驱动模型选 platform driver 最自然。设备树里的 compatible 字符串一匹配,probe 函数就被调用,之后在 probe 里获取中断号、注册中断、初始化硬件,一气呵成。

有人可能想直接用 module_init 里写死中断号,我劝你别这么做。不同版本的 BSP、不同内核,GIC 的中断号映射可能会有调整。用设备树描述硬件,驱动只关心“拿到中断号、注册中断”,可移植性和可维护性都会好很多。

2 驱动的基础设施:设备树匹配、probe 与中断号获取

进入代码之前,先把驱动和设备树之间的约定讲透。这一步如果不对,后面所有代码都白搭。

2.1 设备树节点设计

之前在系列第三篇里应该已经搭好了设备树节点,这里我补一个典型的节点设计,方便驱动这边对齐:

/ { fpga_irq_ctrl: fpga-irq-ctrl@0x10000000 { compatible = "example,fpga-irq-ctrl"; reg = <0x0 0x10000000 0x0 0x1000>; interrupts = <0 17 4>; interrupt-parent = <&intc>; }; };

几个字段的说明:

  • compatible:字符串,驱动里of_device_id匹配的依据,驱动必须一模一样。
  • reg:寄存器基地址和地址范围。这里假设 FPGA 侧映射到 HPS 地址空间 0x10000000,实际地址取决于你在系统里的地址分配。
  • interrupts:三个值分别是中断类型(0 表示 SPI)、中断号(17)、触发方式(4 表示IRQ_TYPE_LEVEL_HIGH)。
  • interrupt-parent:指向中断控制器节点,一般是&intc,即 GIC。

需要注意的是,Agilex 5 的 GIC 和之前 Cyclone V / Arria 10 有些差异,SPI 中断号偏移基准可能不同。别想当然照抄旧工程的数值,先查对应 SoC 的手册,或者用/proc/interrupts验证。

2.2 platform_driver 框架和匹配方式

内核里驱动注册的入口是module_platform_driver()。它把platform_driver_register()module_exit()的注销封装在一起,代码比较简洁:

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of_device.h> #include <linux/interrupt.h> #include <linux/io.h> #include <linux/slab.h> #include <linux/sysfs.h> #include <linux/irq.h> #define DRV_NAME "fpga-irq-ctrl" struct fpga_irq_ctrl { void __iomem *base; int irq; atomic_t irq_count; }; static const struct of_device_id fpga_irq_ctrl_of_match[] = { { .compatible = "example,fpga-irq-ctrl" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, fpga_irq_ctrl_of_match); static int fpga_irq_ctrl_probe(struct platform_device *pdev); static int fpga_irq_ctrl_remove(struct platform_device *pdev); static struct platform_driver fpga_irq_ctrl_driver = { .probe = fpga_irq_ctrl_probe, .remove = fpga_irq_ctrl_remove, .driver = { .name = DRV_NAME, .of_match_table = fpga_irq_ctrl_of_match, }, }; module_platform_driver(fpga_irq_ctrl_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("FPGA IRQ Controller Driver for Agilex5 HPS");

这段代码已经把骨架搭好了。probe里要完成三件事:映射寄存器地址、获取中断号、注册中断处理函数。校验失败时记得返回负的错误码,否则内核会认为驱动初始化成功,后面一运行就出各种怪问题。

2.3 probe 函数的实现

static int fpga_irq_ctrl_probe(struct platform_device *pdev) { struct fpga_irq_ctrl *priv; struct resource *res; int ret; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; platform_set_drvdata(pdev, priv); atomic_set(&priv->irq_count, 0); res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(&pdev->dev, "failed to get memory resource\n"); return -ENXIO; } priv->base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); priv->irq = platform_get_irq(pdev, 0); if (priv->irq < 0) { dev_err(&pdev->dev, "failed to get irq: %d\n", priv->irq); return priv->irq; } ret = devm_request_threaded_irq(&pdev->dev, priv->irq, NULL, fpga_irq_handler_thread, IRQF_TRIGGER_HIGH | IRQF_ONESHOT, DRV_NAME, priv); if (ret) { dev_err(&pdev->dev, "failed to request irq %d: %d\n", priv->irq, ret); return ret; } dev_info(&pdev->dev, "probe done, irq=%d\n", priv->irq); return 0; }

这里我用了devm_前缀的资源管理函数,好处是错误处理和 remove 时可以少写一堆 cleanup,驱动卸载时内核自动帮忙释放。非常适合缩短代码、减少内存泄漏风险。

2.4 中断处理函数与线程化中断

上面注册用的devm_request_threaded_irq()第一个 handler 传了 NULL,这时内核使用默认的 hardirq handler,直接唤醒线程。第二个参数就是我们的线程化处理函数:

static irqreturn_t fpga_irq_handler_thread(int irq, void *dev_id) { struct fpga_irq_ctrl *priv = dev_id; u32 status; status = ioread32(priv->base); dev_info(&priv->dev, "FPGA interrupt triggered, status=0x%x\n", status); atomic_inc(&priv->irq_count); // 在这里做数据读取、通知用户态等耗时操作 // 写确认寄存器,清除 FPGA 侧中断标志 iowrite32(status, priv->base + 0x04); return IRQ_HANDLED; }

dev_id这个参数极其重要。如果注册多个中断或者多个设备,它用来区分是哪个设备触发的。如果你在 handler 里只通过irq号判断设备,在共享中断的情况下会出问题。另一个重要作用是传给内核一个私有数据指针,我们这里直接把priv结构体传进去了。

提到IRQF_ONESHOT,这是配合线程化中断常用的标志。它的含义是:中断线程执行期间,自动屏蔽当前中断线,线程处理完后再重新使能。这样就不用担心中断嵌套导致的数据混乱。对于电平触发的中断,IRQF_ONESHOT尤其重要,因为如果你不及时清 FPGA 侧中断,硬件会一直拉高,内核会反复触发中断,导致中断风暴。

3 寄存器访问与中断处理细节

前面代码里出现了ioread32iowrite32,这是 Linux 内核里访问外设寄存器的标准接口。这里我展开讲讲访问 FPGA 侧寄存器时容易踩的坑,以及中断处理函数里该做什么、不该做什么。

3.1 地址映射是第一步

FPGA 侧的软逻辑通过 AXI 总线接到 HPS 的地址空间,硬件上你要确保编译出来的 RTL 里,这个外设确实可以通过某个地址访问。驱动拿到设备树里的reg之后,用devm_ioremap_resource()做一个虚拟地址映射,之后所有读写都走虚拟地址,不能直接拿物理地址去解引用。

有个非常隐蔽的坑:Agilex 5 的 HPS 有多个地址映射域,FPGA 侧的地址可能落在某个特定的 bridge 后面。如果设备树里的地址和 FPGA Manager 配置的地址不一致,驱动读出来的寄存器全是 0xFFFFFFFF 或者 0x00000000,中断自然也不会触发。排查这个问题时,你可以在驱动里先写一个固定值到某个寄存器,再读回来看看对不对,甚至先用devmem2或者busybox devmem在用户态测一下物理地址能不能访问通。

3.2 中断处理函数中哪些事情不能做

中断线程化之后,虽然可以在 handler 里调用那些可能睡眠的函数,比如kfreemutex_lock、甚至msleep,但这并不意味着可以肆无忌惮。中断线程的优先级通常很高,你在这里耗太久会拖慢整个系统。

一般建议:

  • 上半部只清中断、记录时间戳、从 FIFO 搬运数据到内存缓冲区。
  • 下半部做业务逻辑、唤醒用户态进程、上报事件。
  • 如果数据量很大,考虑用 kfifo 做缓冲,或者干脆交给中断线程里启动的 workqueue。

下面给一段相对完整的处理示例,假设 FPGA 侧有一个 FIFO,里面存了采集到的数据:

#define FPGA_STATUS_REG 0x00 #define FPGA_IRQ_ACK_REG 0x04 #define FPGA_FIFO_DATA_REG 0x08 #define FPGA_FIFO_COUNT_REG 0x0C static irqreturn_t fpga_irq_handler_top(int irq, void *dev_id) { struct fpga_irq_ctrl *priv = dev_id; u32 status; int count; int i; status = ioread32(priv->base + FPGA_STATUS_REG); if (!(status & 0x1)) return IRQ_NONE; count = ioread32(priv->base + FPGA_FIFO_COUNT_REG); for (i = 0; i < count; i++) { u32 data = ioread32(priv->base + FPGA_FIFO_DATA_REG); // 塞进 kfifo,交给下半部 } // 告诉 FPGA:中断已经响应,可以清标志 iowrite32(0x1, priv->base + FPGA_IRQ_ACK_REG); atomic_inc(&priv->irq_count); return IRQ_HANDLED; }

这段代码强调了一点:无论怎么设计,都必须确保 FPGA 侧的中断标志最终被清除。有时候 FPGA 侧的 IP 需要主机写 ACK 寄存器,有时候是 FPGA 侧逻辑读走了最后一个数据后自动拉低,还有时候是软件写 1 清 0。这个行为必须在设计 FPGA IP 的时候定好,并且驱动和 RTL 要严格对齐。

3.3 触发方式的选择

设备树里interrupts最后一个字段和驱动里的IRQF_TRIGGER_*必须一致。常见的有:

  • IRQ_TYPE_LEVEL_HIGH/IRQF_TRIGGER_HIGH:高电平触发。
  • IRQ_TYPE_LEVEL_LOW/IRQF_TRIGGER_LOW:低电平触发。
  • IRQ_TYPE_EDGE_RISING/IRQF_TRIGGER_RISING:上升沿触发。
  • IRQ_TYPE_EDGE_FALLING/IRQF_TRIGGER_FALLING:下降沿触发。

FPGA 和 HPS 之间的中断,我一般偏好电平触发,因为边缘触发容易丢中断,特别是 FPGA 侧的中断信号持续时间很短、而 CPU 正好屏蔽中断的时候。

电平触发时,只要 FPGA 侧中断标志没有清掉,中断线就一直保持有效,内核在 level 模式下会自动重新触发,直到驱动清掉标志。这既是优点也是缺点,优点是不会丢中断,缺点是如果驱动写得不对,就会导致中断风暴,CPU 一直被中断占满。

4 编译、加载与实测验证方法

写到这里,驱动代码已经能完成基础功能,但要让它真正跑起来还差一环:编译通过、加载成功、中断真正触发。下面的内容都是我已经在工程里验证过的流程,照着做能省不少时间。

4.1 创建驱动目录和 Makefile

假设驱动文件叫fpga_irq_ctrl.c,放在某个目录下,同目录创建一个 Makefile:

obj-m := fpga_irq_ctrl.o KDIR := /path/to/your/kernel/build/directory PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean

这里最麻烦的是KDIR。如果你是在 Agilex 5 的开发板上跑 Yocto 构建的内核,建议直接在构建主机上用内核源码目录来编译模块,并使用与目标板内核完全一致的配置。KDIR指向内核源码树,并且里面生成了Module.symversinclude/config等文件,这样编译出来的模块才能和板上内核的符号版本匹配。

如果目标板上已经装了内核头文件,也可以直接在板上指定KDIR=/lib/modules/$(uname -r)/build。不过 SoC FPGA 开发板空间有时紧张,我还是习惯交叉编译。

4.2 交叉编译和安装

在 Intel SoC FPGA 的 Yocto SDK 环境下,设置环境变量后执行:

source /opt/poky/3.4.6/environment-setup-aarch64-poky-linux make

生成fpga_irq_ctrl.ko后,拷贝到开发板上,加载:

insmod fpga_irq_ctrl.ko

如果一切正常,dmesg 里会看到:

fpga-irq-ctrl fpga-irq-ctrl@10000000: probe done, irq=42

看到这一行说明设备树匹配成功、中断号获取成功、中断注册成功。

有时候insmod没报错,但 dmesg 里没有 probe 的打印,大概率是设备树里没有这个节点,或者compatible和驱动里的of_device_id对不上。用下面的命令确认:

ls /proc/device-tree/fpga-irq-ctrl@10000000/ cat /proc/device-tree/fpga-irq-ctrl@10000000/compatible

如果能看到 compatible 文件且内容正确,说明设备树节点没问题。

4.3 如何确认中断真的被触发

手动让 FPGA 侧产生一个中断,然后用工具观察中断计数。这一步在工程联调时非常重要,而且比很多人想得要麻烦。

先看中断注册后的计数:

cat /proc/interrupts | grep fpga-irq-ctrl

正常情况下你会看到一行,比如:

42: 0 GIC-0 181 Level fpga-irq-ctrl

第一列42是 Linux 内部的中断号,0是触发次数。让 FPGA 侧拉高中断后,再执行一次,如果数字没有变化,说明中断根本没有到达内核。

这种时候不建议瞎蒙,先排查几处:

  1. FPGA 侧的中断源是不是真的拉高了?用 Signal Tap 或者逻辑分析仪看。
  2. 中断信号有没有正确连接到 HPS 对应的引脚?查 Pin Mux 配置。
  3. 设备树的中断号有没有和硬件设计对应上?查手册确认 SPI 中断号。

如果/proc/interrupts计数确实在涨,但驱动里的处理函数没执行,多半是中断被别的驱动抢注了,或者你注册的 handler 返回IRQ_NONE导致内核认为这不是你的中断。

另一个简单好用的验证方法是,在用户态用devmem读取驱动里的中断计数寄存器。如果你在驱动里对外开放了 sysfs 属性,也可以直接读。比如:

cat /sys/devices/platform/soc/10000000.fpga-irq-ctrl/irq_count

为了支持这种验证方式,可以在驱动里简单实现一个 sysfs 文件,代码量很少,但联调时非常实用:

static ssize_t irq_count_show(struct device *dev, struct device_attribute *attr, char *buf) { struct fpga_irq_ctrl *priv = dev_get_drvdata(dev); return sprintf(buf, "%d\n", atomic_read(&priv->irq_count)); } static DEVICE_ATTR_RO(irq_count);

然后在 probe 结尾调用:

ret = device_create_file(&pdev->dev, &dev_attr_irq_count); if (ret) dev_warn(&pdev->dev, "failed to create sysfs file\n");

这种做法在调试阶段价值极大,甚至比/proc/interrupts更直观,因为它直接体现的是驱动代码有没有正常执行到。

4.4 一个用户态的触发测试程序

如果你已经可以访问 FPGA 侧寄存器的控制端口,写一个小程序去触发中断也很简单。比如基于 mmap 直接操作物理地址,或者通过已有的 sysfs 接口触发 FPGA 内部逻辑产生脉冲。

我习惯的做法是:在 FPGA 内部做一个小调试寄存器,驱动里通过iowrite32往它写一个值,FPGA 逻辑收到值后立即产生一个中断信号。这样整个链路从“软件写寄存器”到“中断触发”都可以闭环验证,不用依赖外部按钮或者信号发生器。

devmem 0x10000010 32 1

如果这个地址恰好映射到 FPGA 的触发寄存器,那么上面的命令就能产生一次中断。配合watch -n 1 cat /proc/interrupts,基本能立刻看出链路通不通。

5 常见问题与排查技巧实录

驱动能编译、能加载,和驱动能稳定工作之间,隔着几十个坑。下面这些是我在实际调试 Agilex 5 HPS 中断驱动时遇到过的问题,每个都花了不少时间才定位,整理出来希望对你有用。

5.1 probe 成功但中断从未触发

这是最高频的问题。现象是:设备树匹配成功,中断号也打印出来了,但/proc/interrupts里对应的计数永远是 0。

排查步骤,我建议按顺序来:

  1. 检查设备树里的中断触发类型是否和 FPGA 实际信号一致。如果你写的是IRQ_TYPE_LEVEL_HIGH,但 FPGA 实际是低电平有效,GIC 可能根本不会触发。
  2. 检查 GIC 和 FPGA 之间的时钟域。Agilex 5 里 HPS 和 FPGA 经常工作在不同时钟域,中断信号如果没有做同步,GIC 采不到有效电平。
  3. 确认中断信号到了哪个 GIC 分组。SPI 中断是发给哪个 CPU 的?如果亲和性设置到了离线 CPU,也可能触发不到。
  4. 在 FPGA 侧用 Chip Planner / Signal Tap 观察信号,确认 HPS 端接收引脚确实有电平变化。

很多次我把时间浪费在驱动代码上,最后发现是 RTL 里中断信号压根没连接出来,或者连接到了错误的引脚分组。所以第一步永远是先用 Signal Tap 确认物理信号,再谈软件。

5.2 中断风暴,CPU 占用 100%

如果你看到 dmesg 刷屏,D state 的进程变多,系统响应卡顿,多半是中断风暴。最典型的原因就是:电平触发模式下,中断处理函数没有把 FPGA 侧中断标志清掉,或者清标志操作没生效。

此时/proc/interrupts里的数字会非常快地增长。最快的定位办法:临时把触发方式改成IRQF_TRIGGER_LOW或者IRQF_TRIGGER_EDGE,观察风暴是否消失。如果消失,基本可以确认是电平触发 + 未清标志的问题。

还有一种不太常见但很恶心的情况:中断处理函数里清的是 FPGA 的寄存器,但 FPGA 的中断输出是通过组合逻辑直接拉高的,而不是由状态寄存器控制的寄存器输出。这意味着只要你没把中断源本身灭掉,中断线就一直有效。这种问题必须回 RTL 里改。

5.3 devm_request_threaded_irq 返回 -EBUSY

如果返回-EBUSY,表示这个中断号已经被其他设备注册了。最常见的原因是设备树里中断号写重复了,或者你的共享中断没有加IRQF_SHARED

确认方式:

cat /proc/interrupts

查一下这个中断号属于哪个设备。如果确实被别的驱动占用,但你有意共享,必须在request_irq时带上IRQF_SHARED。不过硬件要先确认 FPGA 提供的这个中断源支持共享,否则会带来复杂的互操作问题。

另外注意:使用devm_request_threaded_irq时,如果dev_id传的是priv,那么所有注册该中断的驱动必须传不同的dev_id,这是内核的要求,否则无法区分中断来源。

5.4 platform_get_irq 返回负数

platform_get_irq()返回负数时,dmesg里会有详细错误。常见原因:

  • 设备树节点里没有interrupts属性。
  • interrupt-parent配错,导致内核找不到中断控制器。
  • 中断号超出了 GIC 的范围。

启动阶段用dmesg | grep irq可以快速看到相关警告。如果设备树里interrupts写的是<0 17 4>,但这个 17 号在对应 GIC 里不是有效的 SPI,内核会直接报错。

5.5 模块卸载时崩溃

驱动卸载时崩溃,十有八九是 remove 函数里的顺序问题。要记住一个原则:先注销中断,再释放资源;否则中断刚好在你释放内存之后触发,handler 里访问priv结构体就是访问已经释放的内存,直接 panic。

我用devm_request_threaded_irq时,因为资源由 devm 管理,remove 时内核会按照注册的逆序自动处理。但我仍然建议在 remove 函数里显式调用devm_free_irq或者free_irq的习惯。尤其是你还有自己分配的 DMA 缓冲、kfifo 等资源时,更要小心顺序。

5.6 中断处理时间太长导致其他实时任务卡顿

在 SoC FPGA 上,HPS 和 FPGA 配合时,中断处理往往涉及大量数据搬运。如果每来一次中断,你都在中断线程里读几百 KB 数据,整个系统调度延迟会明显恶化。

这个问题的解法,可以在驱动里引入 NAPI 或者 workqueue 机制。中断线程里只把数据放到 kfifo,并唤醒一个专用 workqueue 来搬数据。实测下来对系统延迟的影响可以降低一个数量级。

6 驱动调试实战:一次完整的中断链路验证

前面讲了很多理论,这一节用一个相对完整的实战流程把整套东西串起来。假设现在硬件已经上电,Linux 已经启动,驱动已经加载,我们需要验证中断链路是否真正打通。

6.1 启动阶段的检查

先看 dmesg 中的打印:

dmesg | grep fpga-irq

期望输出:

fpga-irq-ctrl fpga-irq-ctrl@10000000: probe done, irq=42

如果没有任何输出,先ls /sys/bus/platform/devices/看看有没有生成对应的设备目录。如果设备目录都没有,说明设备树匹配就没成功。

如果设备目录存在但没有 probe 打印,检查compatible字符串是否完全一致,包括大小写和标点。

6.2 寄存器读写自检

在驱动里加一个 debugfs 或 sysfs 文件,暴露一个寄存器读写测试接口。没有的话,直接用devmem读写物理地址。

假设寄存器基地址是 0x10000000:

devmem 0x10000004 32 0xDEADBEEF devmem 0x10000004 32

如果第二次能读出0xDEADBEEF,说明 HPS 到 FPGA 的读写通路是通的。这一步通了,驱动里的ioread32/iowrite32才有意义。

6.3 软件触发中断

在 FPGA 侧加一个“写任意值到某个偏移就产生中断”的调试寄存器。RTL 里类似:

always @(posedge clk) begin if (axi_awvalid && axi_awaddr == TRIGGER_REG) begin irq_pulse <= 1'b1; end else begin irq_pulse <= 1'b0; end end assign irq_out = irq_pulse;

然后用户态执行:

devmem 0x10000010 32 1

此时立即查看/proc/interrupts

cat /proc/interrupts | grep fpga-irq

如果中断数增长,且 dmesg 里出现了中断处理函数里的打印,说明链路完全打通。

6.4 性能粗测

中断链路打通后,建议做一个最简单的性能粗测。比如记录每次中断到处理函数的时间戳差值,或者在处理函数里统计每秒中断次数。

static irqreturn_t fpga_irq_handler_thread(int irq, void *dev_id) { struct fpga_irq_ctrl *priv = dev_id; u64 now = ktime_get_ns(); // 记录到环形缓冲区,方便统计 priv->timestamp[priv->idx & 0xFF] = now; priv->idx++; // ... 业务处理 return IRQ_HANDLED; }

对于高速中断,比如 FPGA 以 10 kHz 频率产生中断,每个中断间隔是 100 微秒。如果你的处理时间超过这个值,系统就会被拖垮。这个基线数据很重要,后面做数据搬运优化时可以用来进行数据对比。

7 经验总结与后续扩展

项目做到这个阶段,“FPGA 中断到 HPS 驱动能正确响应”这个目标已经达成。但说实话,这只是 SoC FPGA 协同开发的第一步,后面还有大量可以扩展的地方。

比如,如果你的 FPGA 侧有多个中断源,可以考虑用一个共用中断线,在驱动里读状态寄存器区分中断源;也可以给每个中断源分配独立 SPI,但要注意 GIC 的中断资源是有限的。

再比如,实际产品中往往需要把中断事件上报到用户态。这里可以有多种方案:/dev节点配合poll/epoll、netlink socket、或者信号机制。我最常用的是在驱动里注册一个miscdevice,用户态用epoll等待POLLIN事件,中断到来时在驱动里调用wake_up_interruptible()唤醒等待队列。

这段代码也不复杂:

struct fpga_irq_ctrl { // ... wait_queue_head_t read_wait; }; static ssize_t fpga_irq_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { struct fpga_irq_ctrl *priv = filp->private_data; u32 val; if (wait_event_interruptible(priv->read_wait, atomic_read(&priv->irq_count) > 0)) return -ERESTARTSYS; val = atomic_read(&priv->irq_count); atomic_set(&priv->irq_count, 0); if (copy_to_user(buf, &val, sizeof(val))) return -EFAULT; return sizeof(val); }

我之前有个项目就吃过用户态轮询的亏:FPGA 侧中断频率一高,用户态usleep轮询方式根本忙不过来,CPU 占用率直接冲到 90% 以上。改成 epoll 等待之后,占用率掉到个位数。这个经验在 SoC FPGA 的驱动开发里非常值得记住。

另外还要提一下 DMA 的联动场景。很多 FPGA 采集应用里,中断只是“数据准备好了”的信号,真正的大块数据是通过 DMA 从 FPGA 搬走的。那种情况下,中断处理函数要做的往往是启动 DMA 传输,而不是在中断里一点一点搬数据。用 ARM 的 DMA 引擎配合dmaengine_prep_slave_single()或者dma_alloc_coherent(),可以把中断的开销降到很低。

最后说一点最底层的体会:写 SoC FPGA 的驱动,最忌讳的就是只盯着软件代码。中断不触发,很多时候是 FPGA 侧信号的问题;寄存器读出来不对,很多时候是地址映射或者 AXI 时序的问题。软硬件联调时,一定要先确认物理信号,再排查驱动逻辑。养成这种调试习惯,你在 Agilex 5 这类 SoC FPGA 平台上做开发会顺手很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 5:49:40

Linux内核崩溃调试实战:从Oops到KASAN的完整排查路径

半夜两点被值班电话叫起来&#xff0c;屏幕上滚动着一行熟悉的英文——unable to handle kernel null pointer dereference at virtual address&#xff0c;后面跟着一堆看不懂的寄存器值和调用栈。这种场景对每一个搞Linux内核或者驱动开发的人来说都不陌生。说句实话&#xf…

作者头像 李华
网站建设 2026/9/17 5:49:06

提示词工程实战:10个让大模型输出稳定可控的技巧

提示词工程这几年算是被聊烂了&#xff0c;但真正能把提示词写到“稳定可用”的人&#xff0c;其实并不多。见过太多人拿大模型写东西&#xff0c;开口就是一句“帮我写个方案”&#xff0c;结果出来的内容要么泛泛而谈&#xff0c;要么漏洞百出&#xff0c;最后还要靠自己返工…

作者头像 李华
网站建设 2026/9/17 5:48:20

Windows AI编程生存手册:PowerShell+Node.js原生环境实战

1. 这不是“装软件”教程&#xff0c;而是Windows上跑AI编程的生存手册你搜过“Windows AI编程环境”吗&#xff1f;搜出来的结果大概率是&#xff1a;Node.js安装步骤、PowerShell怎么打开、Git下载链接、再加几个带“AI”字样的模糊标题。但真正用Windows做AI开发的人知道——…

作者头像 李华
网站建设 2026/9/17 5:46:42

自举开关与ADC采样电路系统设计:从采样保持到FFT谐波排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华