ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Zynq上AXI-Stream FIFO的Linux驱动开发:从设备树到字符设备实践

Zynq上AXI-Stream FIFO的Linux驱动开发:从设备树到字符设备实践 简介面向Xilinx Zynq SoC平台上的AXI-Stream FIFO IP这份Linux内核驱动及构建工程为嵌入式开发者提供了从硬件寄存器操作到用户空间数据通路的完整实现。AXI-Stream协议是Xilinx高性能数据流接口FIFO IP常承担缓存与速率匹配职责驱动则确保Linux能正确识别和控制该硬件。压缩包共10个文件以C源文件为主涵盖驱动主模块、回环测试程序、以太网桥接测试、Makefile构建脚本、头文件以及README与LICENSE说明整体仅35KB结构紧凑。已有285人学习下载。驱动代码覆盖设备初始化、数据读写、中断处理与错误恢复等关键环节Makefile便于直接编译为可加载内核模块配套测试程序可验证回环与以太网桥接场景帮助深入理解AXI-Stream协议在Linux设备模型中的注册与数据交互方式同时参考如何使用sysfs或/dev接口向用户空间暴露硬件能力。对于正在学习Zynq PL-PS协同设计或需要快速适配自有IP的开发者这份资源提供了可直接借鉴的内核模块实现与工程组织方式。1. 别再用 devmem 直接戳寄存器了这份 Zynq 上的 AXI-Stream FIFO 驱动包能让你少走三个月弯路Zynq 上的 Xilinx AXI-Stream FIFO 驱动是典型的问的人多、写对的人少的内核模块。FPGA 侧把数据沿着 AXI-Stream 总线灌进 FIFOPS 侧 Linux 想读数据很多人的第一反应是拿 devmem 去敲物理地址。devmem 只能验证寄存器通了没有真正要搬大块数据、要处理中断、要和用户态程序对接必须有正经的内核驱动。这套源码包提供的就是一个独立的 C 语言字符设备驱动外加一套 Makefile主体面向 Zynq SoC 上挂接 AXI-Stream FIFO IP 的场景覆盖了设备树匹配、物理地址映射、中断注册、file_operations 读写这一整条链路适合做数据采集、协议转换、高速接口桥接的嵌入式 Linux 工程师。新手照着改 compatible 字符串就能跑起来熟手拿来改造成 DMA 驱动也不费劲。2. 驱动和 IP 是怎么互相认识的寄存器视图、设备树与 platform_driver 的握手顺序2.1 AXI-Stream FIFO 的 AXI4-Lite 寄存器视图CPU 凭什么能直接读写这个 FIFOXilinx 的 AXI-Stream FIFO IP 有几种形态Zynq Linux 驱动里最常碰到的是带有 AXI4-Lite 从接口的那一类FPGA 侧掏 FIFO 深度、握手信号、TLAST 边界这些事CPU 侧通过一组寄存器窗口去控制。这个 IP 的寄存器布局并不复杂核心思路是你把 FIFO 当成一块内存写入数据就往某个偏移地址搬读出数据就从另一个偏移地址取FIFO 的空满状态由状态寄存器反映中断挂起和屏蔽位各自独立。这个设计决定了驱动的基本框架——不需要把 FIFO 抽象成 block 设备一个字符设备就够了。实际驱动里对寄存器做映射时我一般会用 devm_ioremap_resource 而不是裸的 ioremap好处是设备树里 reg 属性和请求的资源能自动做合法性校验资源冲突、地址越界会在 probe 阶段直接暴露不会等 read 的时候才崩。映射粒度上AXI4-Lite 访问最小是 4 字节对齐所以 reg 里的地址一定要按 0x10000 这种粒度留给 IP不要和别的外设挤在一个页里。static int axififo_probe(struct platform_device *pdev) { struct resource *res; struct axififo_dev *fifo; fifo devm_kzalloc(pdev-dev, sizeof(*fifo), GFP_KERNEL); if (!fifo) return -ENOMEM; res platform_get_resource(pdev, IORESOURCE_MEM, 0); fifo-base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(fifo-base)) return PTR_ERR(fifo-base); fifo-irq platform_get_irq(pdev, 0); if (fifo-irq 0) return fifo-irq; platform_set_drvdata(pdev, fifo); return axififo_setup_cdev(fifo, pdev-dev); }这里有三处值得说明。platform_get_resource 取的是设备树里 reg 的第一个区间如果 DTS 里写了两个 regIP 核本身有独立地址空间的优先把控制寄存器那一组放前面。devm_ioremap_resource 会自动处理 resource 的 startup 和内存区域合法性检查返回的虚拟地址可以直接拿 readl/writel 去访问。platform_get_irq 对应的是 interrupts 属性拿到的 irq 号给 request_irq 用Zynq 上 GIC 分发的中断号已经是 Linux 这边的内部编号不需要再做转换。2.2 compatible 字符串、file_operations 和中断上下文的配合驱动骨架为什么这么搭驱动能不能被自动加载取决于设备树节点的 compatible 是否和驱动里 of_match_table 中的某项一致。这个字符串不是随便写的Vivado 生成设备树时通常会给这个 IP 生成形如 xlnx,axi-fifo-1.00.a 之类的 compatible。如果你在 SDK 或 Vitis 里看到的是另一个名字直接照抄它不要自己发明。这个匹配是 platform_bus 上设备与驱动互认的身份证对不上probe 就没有机会执行。static const struct of_device_id axififo_of_match[] { { .compatible xlnx,axi-fifo-mm-s-4.1, }, {}, }; MODULE_DEVICE_TABLE(of, axififo_of_match); static struct platform_driver axififo_driver { .probe axififo_probe, .remove axififo_remove, .driver { .name axififo, .of_match_table axififo_of_match, }, }; module_platform_driver(axififo_driver);拿到硬件资源之后剩下的就是把数据通路暴露给用户态。file_operations 里最核心的是 read、write有需要还可以加 mmap 和 poll。read 这边要考虑 FIFO 空的问题用户态 read 一个空 FIFO驱动不能傻乎乎地返回 0否则应用层会误以为流结束了。常见做法是让 read 在没有数据时进睡眠用 wait_event_interruptible 挂在 FIFO 的非空条件上等中断来了或者轮询发现新数据再唤醒。写方向的逻辑相反FIFO 满了就等非满条件。中断服务程序里只做一件事——唤醒等在条件变量上的进程实际的数据拷贝放到进程上下文里做。static ssize_t axififo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct axififo_dev *fifo filp-private_data; u32 val; int ret; ret wait_event_interruptible(fifo-readq, !axififo_is_empty(fifo-base)); if (ret) return -ERESTARTSYS; val ioread32(fifo-base FIFO_DATA_OFFSET); if (copy_to_user(buf, val, sizeof(val))) return -EFAULT; return sizeof(val); }注意这里返回的是单次 4 字节。实际项目里如果要一次读一大块就得加上循环和剩余计数并且考虑用 ioread32_rep 这类批量读的变体效率差好几倍。中断服务程序里最忌讳做 copy_to_user那是在原子上下文里碰用户页八成要 oops。我在早期版本里干过这事后来全改成中断只置标志、唤醒等待队列数据搬运全部归还给 read/write 的进程上下文。这个结构虽然简单但恰好是 Zynq 上大多数 AXI-Stream FIFO 控制类驱动的基本原型。3. Makefile 与交叉编译的细节把源码编成 .ko 的三步和三个检查点3.1 KBUILD 体系下这套 Makefile 的工作原理这套驱动包里的 Makefile 走的是内核模块的标准构建方式核心语法就是 obj-m 指定要编的模块名然后调用内核源码树的 Makefile 来接管后续步骤。交叉编译时最关键的四个变量是 ARCH、CROSS_COMPILE、KERNELDIR 和 INSTALL_MOD_PATH前两个告诉 Kbuild 你在为哪个架构、用哪套工具链编译后两个决定中间产物去哪找、编完的 .ko 安装到哪。obj-m : axififo.o KERNELDIR ? /lib/modules/$(shell uname -r)/build CROSS_COMPILE ? arm-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc ARCH : arm all: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCH$(ARCH) \ CROSS_COMPILE$(CROSS_COMPILE) modules install: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCH$(ARCH) \ CROSS_COMPILE$(CROSS_COMPILE) modules_install depmod -b $(INSTALL_MOD_PATH) $(KERNELRELEASE) clean: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCH$(ARCH) \ CROSS_COMPILE$(CROSS_COMPILE) clean这里有个隐性的坑KERNELDIR 指向的内核源码树必须是和 Zynq 目标板上运行的内核同源、同配置编译出来的最好不要拿 Ubuntu 的发行版内核头文件来顶替。因为 KBUILD 会校验 vermagic编译出来的模块带着一组版本魔术字insmod 时只要和当前运行内核的任一字段不匹配就直接拒绝加载。很多人的驱动在 PC 上编译通过拿到板子上 insmod 报 Exec format error 或 Invalid module format都是栽在这。3.2 编译之前必须检查的三样东西少一样你都可能在浪费一整天第一样是工具链。Zynq 是 ARM Cortex-A9常见的是 arm-linux-gnueabihf 这套但这不是死的。有些老工程用 arm-xilinx-linux-gnueabi本质是同一个工具链换个前缀。你如果拿到的是另一套工具链只需改 CROSS_COMPILE 的前缀不要动 obj-m 和 MODULE 名。第二样是内核源码里的配置。zynq 的内核通常开了 CONFIG_MODULES这是模块加载的前提。但模块能不能编出来还取决于内核源码里 include/generated/autoconf.h 是否和你目标板一致最稳妥的做法是拿目标板上 /proc/config.gz 解压出来的 .config 放到内核源码目录执行 make oldconfig再 make modules_prepare。这一步做完基本能保证 vermagic 和符号表都对得上。# 从目标板拷贝内核配置到源码树根目录 scp root192.168.1.10:/proc/config.gz . gunzip -c config.gz .config make ARCHarm oldconfig make ARCHarm modules_prepare第三样是模块安装目录。板上根文件系统如果是 ramdisk 启动的把 .ko 丢进去之前要想清楚重启后是否保留。如果是 SD 卡启动模块通常放在 /lib/modules/$(uname -r)/extra 下面insmod 或 modprobe 时按这个路径查找。这块我之前吃过亏驱动编好了、insmod 也过了但 reboot 后模块没了后来又用 modprobe 自动加载才发现 DEPMOD 生成的 modules.dep 一直没更新。4. 装驱动到跑数据设备树节点、insmod 命令和应用层读写例程4.1 设备树节点怎么配reg 和 interrupts 必须和 Vivado 工程严格对应设备树是 Zynq 上 PS 与 PL 外设之间的合同。AXI-Stream FIFO 如果挂在一个 AXI GP 口下面物理地址由 Vivado 的 Address Editor 决定。你在 SDK 里看到的地址是多少DTS 里就得写多少少一位都不行。常见写法是 32 位地址空间里的地址段比如 0xa0000000长度给到 0x10000留足寄存器映射空间。axififo: axififoa0000000 { compatible xlnx,axi-fifo-mm-s-4.1; reg 0xa0000000 0x10000; interrupts 0 29 4; };interrupts 属性里的三个值第一个 0 表示 SPI共享外设中断第二个 29 是中断号第三个 4 是触发类型。这里最容易踩坑的就是第二个数字。Vivado 里中断引脚接到 PS 的 pl_ps_irq 上时编号是 61 开始的但 Linux 的 GIC 驱动里 SPI 中断号是从 32 开始编码的设备树里写的数字等于硬件中断号减 32。你要是从 Vivado 报告里看到 IRQ 是 61DTS 里就要写 29。这个换算关系搞错了驱动 probe 不报错但中断永远不触发读数据直接饿死。我后来每次写完设备树都要先 cat /proc/interrupts 确认注册了没、计数动不动。4.2 应用层 open/read/write 的完整例程和参数语义驱动加载成功后/dev/axififo 就会出现。设备号哪来的要看驱动里注册时用的是动态分配还是固定主设备号。建议用动态分配然后通过 udev 或 mknod 手动创建节点。应用层编程里read 的语义是有多少拿多少和普通文件不同它不带偏移量每次读取从 FIFO 头部弹出数据。write 的语义同理往 FIFO 尾不塞数据。#include fcntl.h #include unistd.h #include stdio.h #include stdint.h int main(void) { int fd, ret; uint32_t buf[256]; int i; fd open(/dev/axififo, O_RDWR); if (fd 0) { perror(open /dev/axififo); return -1; } for (i 0; i 256; i) buf[i] 0xA5000000 | i; ret write(fd, buf, sizeof(buf)); printf(write %d bytes\n, ret); memset(buf, 0, sizeof(buf)); ret read(fd, buf, sizeof(buf)); printf(read %d bytes, first 0x%08x\n, ret, buf[0]); close(fd); return 0; }注意 read 这里只保证最多读 sizeof(buf) 字节如果 FIFO 里数据不足驱动可能返回比请求更少的字节数。所以应用层一定要循环读取不能指望一次 read 拿满。在实时性要求高的场景里我会建议应用层开独立线程阻塞在 read 上一有数据就处理中断唤醒的开销远比应用层轮询小。5. 避坑手册这套驱动最常见的五个翻车现场与排查顺序5.1 insmod 报 Invalid module formatvermagic 是内核模块的第一道玄学现象模块编译零错误拷到板子上 insmod 报 Invalid module format。原因编译用的内核源码树版本、配置和目标板运行内核不一致vermagic 字符串对不上。解决先执行 modinfo axififo.ko 看 vermagic 字段再 cat /proc/version 看运行内核两者必须一致。不一致就按 3.2 节重走一遍 modules_prepare务必用目标板自带的 .config。5.2 寄存器能读但 read 永远超时IP 复位顺序比驱动代码更可能是元凶现象devmem 能读到控制寄存器但驱动 read 一直阻塞dmesg 也没有中断记录。原因AXI-Stream FIFO IP 在 PL 侧没有释放复位或者复位后没等到 FIFO 的初始化完成标志。解决在 Vivado 里确认 IP 的复位信号由 PS 复位控制还是由 PL 逻辑提供驱动 probe 时使用复位宏或复位逻辑确保 FIFO 进入可操作状态。我一般会在 probe 里先写复位寄存器然后读状态寄存器轮询 ready 位超时再报错。5.3 中断号对不上导致完全静默DTS 里 61 和 29 的换算现象驱动加载成功/proc/interrupts 里能看到 axififo 的条目但数据来了计数不动。原因设备树里第三个数字可能写错为硬件中断号或者 SPI 编号没有减 32。解决设备树里 SPI 中断写interrupts 0 29 4对应 Vivado 中 IRQ 编号 61如果是 SGIC 的私有中断则概念不同。验证方法是先读 /proc/interrupts写入数据触发一次观察中断计数是否增加。5.4 数据错位、丢 4 字节io 宽度与 TLAST 的匹配问题现象前几次 read 正常数据流长了以后整体错位或者周期性地丢一个 32 位字。原因FIFO 数据位宽是 32 位但驱动只按 4 字节读当 IP 端把多个 TLAST 事务打包交错时读偏移不对齐。解决核对 IP 里 TDATA 宽度配置驱动里用 ioread32 对齐读取如果 AXI-Stream 侧数据包含 TKEEP、TUSER需要把状态寄存器里的 packet 边界信息一并读出来只在完整包到来时才向上抛数据这点我踩过最深的坑一度怀疑是自己的环形缓冲区写崩了连 kmemleak 都上了最后发现只是漏判 TLAST。5.5 换 SD 卡启动后模块消失modprobe 的 dep 文件没有重建现象模块拷到 /lib/modules/.../extrainsmod 能成功但系统重启后 insmod 报 Module not found。原因根文件系统的模块依赖数据库没有更新或者启动脚本里 depmod 没有执行。解决运行 depmod -a 或 depmod -b / 之后重启再用 modprobe axififo 加载。每次替换内核镜像或模块之后这条命令都要强制走一遍不要省。6. 中断驱动还是轮询驱动用一个 /proc 接口做自测的小技巧驱动装上能跑通只是第一步怎么验证中断通路是否顺畅、数据量是否压得住这才是真正区分能用和好用的地方。debugfs 或 procfs 在这里比一个临时写的应用小程序更省事。我习惯在驱动里加一个 procfs 文件导出 FIFO 的状态寄存器和中断计数应用层或者命令行直接 cat 就能看到内部状态不需要串口停掉业务去调试。static int axififo_proc_show(struct seq_file *m, void *v) { struct axififo_dev *fifo m-private; seq_printf(m, irq_count: %u\n, fifo-irq_cnt); seq_printf(m, fifo_status: 0x%08x\n, ioread32(fifo-base FIFO_STATUS_OFFSET)); return 0; } static int axififo_proc_open(struct inode *inode, struct file *file) { return single_open(file, axififo_proc_show, PDE_DATA(inode)); }在 probe 里用 proc_create_data 注册这个节点rmmod 时用 remove_proc_entry 清理。这样调试时不断在 FPGA 侧灌数据随时 cat /proc/axififo 看中断计数和 FIFO 水线。中断方式的好处是 CPU 占用低、响应快但中断风暴是个隐患轮询方式实现简单、不怕中断风暴但空转浪费 CPU。折中方案是 poll 和中断结合休眠唤醒加上超时兜底这个驱动包的基础结构里已经预留了 waiting queue改成这种混合模式不需要大手术。从那以后我每次拿到新的 AXI-Stream 外设 IP都强制先做三件事写一个最小 probe、导出一个状态节点、确认中断计数会动。三步走完再写业务逻辑基本不会再出现不知道是 FPGA 没发数据还是驱动没读到这种悬案。这套资源最大的价值不是帮你抄代码而是让你知道驱动和 IP 之间的每个握手点在哪希望帮到你。本文还有配套的精品资源点击获取
返回列表