保姆级教程六:告别死等!ZYNQ Linux下PL如何主动向PS发中断?(UIO驱动小白必看)
文章目录
- 保姆级教程六:告别死等!ZYNQ Linux下PL如何主动向PS发中断?(UIO驱动小白必看)
-
- 🔔 核心概念:Linux 下的“智能门铃” —— UIO 驱动框架
- 🛠️ 第一步:Vivado 硬件设计(给 FPGA 拉一根门铃线)
- 🚀 第二步:PetaLinux 配置(告诉 Linux 门铃装在哪)
-
- 1. 开启内核的 UIO 驱动支持
- 2. 修改设备树(最关键的一步!)
- 3. 重新编译打包
- 🔍 第三步:开机验证“门铃”是否安好
- 💻 第四步:C语言实战(躺平等待 FPGA 的呼唤)
-
- 编译运行:
- ❓ 进阶答疑 (FAQ)
在前面的教程中,我们学会了用
/dev/mem +
mmap 的方法,让 PS端(ARM核)去读写 PL端(FPGA)的物理地址。
很多小白学到这里觉得大功告成了,但如果你去公司里写出这样的代码,大概率会被老员工骂:“你这样写,CPU 会被活活累死的!”
为什么?因为 mmap 只能让老板(PS端)主动去问员工(PL端)。假设让 FPGA 去处理一张高清图片,CPU 为了知道它有没有处理完,只能写个 while(1) 死循环,每隔1毫秒去读一次状态寄存器。这叫轮询(Polling),极度浪费系统资源!
今天,我们就来学习更高级、更优雅的玩法:中断(Interrupt)。让 FPGA 干完活后,主动给 CPU “按门铃”!
🔔 核心概念:Linux 下的“智能门铃” —— UIO 驱动框架
在裸机(无系统)状态下,写中断很简单,配几个寄存器就行。但在 Linux 系统里,中断是内核的“绝对禁脔”,普通的应用层 C 语言根本摸不到中断线。
如果要按正规流程,你需要写一套完整的 Linux 字符设备内核驱动。这对小白来说,简直是地狱难度。
好在,Linux 内核的大佬们给咱们留了一道极其好用的“合法后门”——UIO (Userspace I/O) 框架。
💡 小白秒懂 UIO: 如果说上一篇的 /dev/mem 是直接把整个物理内存扒光了给你看(不安全、不支持门铃)。 那么 UIO 就是系统专门为你这个 FPGA 模块建的一个“带门铃的独立办公室”。 你不仅能安全地读写里面的寄存器,当 FPGA 发送中断时,你的 C 语言程序还能听到“叮咚”声!
🛠️ 第一步:Vivado 硬件设计(给 FPGA 拉一根门铃线)
要让 PL 端能发中断,硬件连线必须要有。
🚀 第二步:PetaLinux 配置(告诉 Linux 门铃装在哪)
拿着新的 .xsa 图纸,回到 Ubuntu 里的 PetaLinux 工程。
1. 开启内核的 UIO 驱动支持
输入命令打开内核配置:
petalinux-config -c kernel
进入菜单,按以下路径勾选(按 Y 变成 [*] 或 <*>): Device Drivers -> Userspace I/O drivers -> 勾选 Userspace I/O platform driver with generic IRQ handling。 保存并退出。
2. 修改设备树(最关键的一步!)
Linux 开机时需要查户口,我们需要在设备树里声明:这个硬件,请用 UIO 驱动来接管!
打开 PetaLinux 工程里的设备树追加文件:
vim project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
在里面添加以下代码(假设你的 AXI IP 核名字叫 my_ip_0):
/include/ "system-conf.dtsi"
/ {
};
/* 通过符号引用,覆写 PL 端的设备节点属性 */
&my_ip_0 {
compatible = "generic-uio"; /* 这句是灵魂!告诉系统用 UIO 驱动它 */
};
3. 重新编译打包
petalinux-build
petalinux-package –boot –fsbl images/linux/zynq_fsbl.elf –fpga images/linux/system.bit –u-boot –force
把新的 BOOT.BIN 和 image.ub 拷入 SD 卡,插板开机!
🔍 第三步:开机验证“门铃”是否安好
开机登录 ZYNQ 开发板的终端,输入:
ls /dev/uio*
如果你看到屏幕上输出了 /dev/uio0,恭喜你!这说明 Linux 已经成功把你的 FPGA 模块包装成了一个 UIO 设备!
💻 第四步:C语言实战(躺平等待 FPGA 的呼唤)
以前用 /dev/mem 我们是死循环读,现在用 /dev/uio0,我们可以让程序“睡大觉”,直到中断来了才醒。
新建 uio_test.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#define UIO_DEV "/dev/uio0"
#define MAP_SIZE 4096 // 映射大小
int main() {
int fd;
void *map_addr;
unsigned int irq_count = 0; // 记录中断发生的次数
// 1. 打开 UIO 设备文件
fd = open(UIO_DEV, O_RDWR);
if (fd < 0) {
printf("打开 %s 失败!\\n", UIO_DEV);
return –1;
}
// 2. 映射物理地址 (UIO的映射比 /dev/mem 更安全,不需要填绝对物理地址)
map_addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_addr == MAP_FAILED) {
printf("内存映射失败!\\n");
close(fd);
return –1;
}
volatile unsigned int *pl_reg = (volatile unsigned int *)map_addr;
printf("UIO 初始化成功!程序进入休眠,等待 PL 端发送中断…\\n");
// ==========================================
// 3. 核心机制:阻塞等待中断
// ==========================================
// 开启 UIO 中断:必须往 /dev/uio0 写入一个值为 1 的整数
unsigned int irq_on = 1;
write(fd, &irq_on, sizeof(irq_on));
// read() 会让程序阻塞(死等睡觉),直到 PL 端发来中断,程序才会往下执行!
// 读出的数据是中断发生的总次数
read(fd, &irq_count, sizeof(irq_count));
printf("==================================\\n");
printf("叮咚!收到 PL 端发来的中断信号!\\n");
printf("这是第 %d 次中断。\\n", irq_count);
// 中断发生后,去读取 PL 计算好的数据寄存器 (假设偏移量为 4)
// printf("PL端处理结果为: 0x%X\\n", *(pl_reg + 1));
printf("==================================\\n");
// ==========================================
// 4. 清理现场
munmap(map_addr, MAP_SIZE);
close(fd);
return 0;
}
编译运行:
arm-linux-gnueabihf-gcc uio_test.c -o uio_test
chmod +x uio_test
./uio_test
运行后,程序会停在 等待 PL 端发送中断… 这里,此时 CPU 占用率为 0%! 一旦你的 FPGA 硬件条件满足(比如按键按下、或者定时器到时间),触发了 IRQ_F2P,屏幕上就会瞬间打印出:“叮咚!收到 PL 端发来的中断信号!”
❓ 进阶答疑 (FAQ)
Q1:既然 UIO 这么好用,那上一篇的 /dev/mem 是不是白学了?
- 不白学! UIO 的本质依然是物理内存映射,但它是更高级、更安全的封装。/dev/mem 适合快速调试、直接强改寄存器;而 UIO 适合做正式项目的应用层驱动。公司里绝大多数不需要海量数据传输的自研 IP 核,都是用 UIO 控制的!
Q2:如果我还要传大量图像数据怎么办?UIO 够用吗?
- UIO 只能传控制指令和小数据(寄存器级别)。 如果你要从 PS 搬运 1080P 视频到 PL 进行图像处理,单靠 UIO 会卡死。这就需要祭出 ZYNQ 的终极大杀器:AXI DMA (直接内存访问)。这不仅需要 UIO,还需要分配连续物理内存。
结语: 从“死循环轮询”到“UIO 阻塞中断”,你的 ZYNQ Linux 开发水平已经完成了从“初学者”到“入门工程师”的蜕变!掌握了 UIO,你就能驾驭 80% 的日常软硬协同开发任务。




