Zynq 实战 11|PL 自定义 IP 的 Linux 驱动实战:UIO、/dev/mem 与内核驱动三条路
Zynq 实战 11|PL 自定义 IP 的 Linux 驱动实战
这是《Zynq FPGA 嵌入式系统设计实战》系列的第 11 篇。 板子:Pynq-Z2(XC7Z020)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 10|PetaLinux 根文件系统定制》
0. 这一篇要解决什么问题
第 06 篇我们在 PL 里做了一个 AXI-Lite 接口的 PWM IP,Vivado 仿真过了,也用 Vitis 裸机控制过了。现在 PS 这边跑了 Linux,问题变成:怎么从 Linux 用户态去读写 PL IP 的寄存器?
这一篇给三条路:
- UIO(Userspace I/O):把寄存器 mmap 到用户态,
read()等中断,最简单,生产环境可用 - /dev/mem + mmap:两行代码就能访问任意物理地址,调试快刀,生产禁器
- 完整内核字符设备驱动:
file_operations + ioremap,最复杂,功能最完整
本文不讲”字符设备驱动是什么”——默认你知道 Linux 驱动的基本概念。本文讲的是:在真实 Zynq 工程里,这三条路怎么走、各自踩什么坑。
本文不覆盖 DMA 引擎(AXIDMA/VDMA)驱动——那是下一篇的内容。
1. 先把物理地址从 Vivado Address Editor 里找出来
在写任何驱动之前,你要知道 PL IP 被分配到了哪个物理地址。这个地址由 Vivado 的地址分配器决定,在 Address Editor 里查。
在 Vivado Block Design 界面:菜单 Window → Address Editor,或者直接在 Diagram 画布里点任意 AXI-Lite 从机 IP,Tcl Console 会打印它的地址。
以第 06 篇的 PWM IP 为例,连在 M_AXI_GP0 上的情况下,典型的分配结果是:
| IP 名称 | 接口 | 基地址 | 范围 | 高地址 |
|---|---|---|---|---|
pwm_ctrl_0 | S_AXI | 0x43C0_0000 | 64K | 0x43C0_FFFF |
🚧 避坑:地址范围(Range)是 Vivado 分配时让你选的,一般选 64K(0x10000) 就够,哪怕你的 IP 只有 4 个 32-bit 寄存器(16 字节)。因为 Linux mmap 是 PAGE_SIZE(4096 字节)对齐的,选太小会造成设备树和 mmap 不一致。另外,Vivado 分配的地址和 Linux 设备树里的
reg属性必须完全一致——如果你修改了 Block Design 重新分配,设备树也要同步改,否则驱动 probe 时拿到的地址会映射到错误的物理空间。
Vivado 里确认地址后,记下两个数字:
- 基地址:
0x43C00000 - 范围:
0x10000(64KB)
这两个数字后面会反复出现在设备树、mmap() 调用和驱动代码里。
2. 三条路对比
用一句话概括三者的本质区别:
| 方法 | 内核参与度 | 数据路径 | 中断支持 | 生产可用 | 难度 |
|---|---|---|---|---|---|
| UIO | 最低(只处理中断) | 用户态直接 mmap 寄存器 | ✅ read() 阻塞等 IRQ | ✅ | ⭐⭐ |
| /dev/mem | 无(内核内置驱动) | 用户态直接映射物理地址 | ❌ 无法使用 | ❌ | ⭐ |
| 完整内核驱动 | 最高(全部在内核) | 系统调用 → 内核 → 硬件 | ✅ request_irq | ✅ | ⭐⭐⭐⭐ |
UIO 的核心优势是:寄存器读写这条数据路径根本不过内核——你 mmap 之后,reg[0] = 0x01 这种操作直接翻译成一条 AXI 总线写事务,中间没有系统调用,没有上下文切换,延迟比完整驱动低得多。中断这条路才需要read() 进内核等一下。
3. UIO 完整流程
3.1 内核配置:打开 CONFIG_UIO_PDRV_GENIRQ
PetaLinux 2023.2 的 BSP 默认不一定开这个选项,需要手动确认:
# 在 PetaLinux 工程目录下
petalinux-config -c kernel
进入 menuconfig 后,搜索 UIO(按 / 键):
Device Drivers
└── Userspace I/O drivers
├── [*] Userspace I/O platform driver with generic IRQ handling <-- CONFIG_UIO_PDRV_GENIRQ=y
└── [*] Userspace I/O platform driver with generic irq and dynamic memory
把 UIO_PDRV_GENIRQ 选成 [*](编译进内核),或者 [M](模块,需要手动 modprobe uio_pdrv_genirq)。
推荐直接编译进内核([*]),省一步 modprobe。
🚧 避坑:很多教程会说”UIO 直接可以用”。在 Xilinx 官方 BSP 里不一定——我实际测试 PetaLinux 2023.2 的 BSP 里
CONFIG_UIO_PDRV_GENIRQ默认是M(模块),你不modprobe就不会出现/dev/uio0。建议在system-user.dtsi里指定compatible = "generic-uio"后,先dmesg | grep uio确认驱动有没有 probe 成功。
3.2 设备树节点
PetaLinux 2023.2 的设备树定制文件是:
<petalinux-project>/project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
在这个文件里为 PWM IP 加节点:
/* system-user.dtsi — PetaLinux 2023.2,对应 Vivado Address Editor 分配 */
/ {
amba_pl: amba_pl@0 {
#address-cells = <1>;
#size-cells = <1>;
ranges;
pwm_ctrl: pwm@43c00000 {
compatible = "generic-uio"; /* 触发 uio_pdrv_genirq 驱动 */
reg = <0x43c00000 0x10000>; /* 基地址 0x43C00000,大小 64KB */
interrupt-parent = <&intc>; /* Zynq GIC,在 pl.dtsi 里定义 */
interrupts = <0 29 4>;
/* ↑ 三元组含义:
* 0 = SPI(Shared Peripheral Interrupt,PL 到 GIC 都是 SPI)
* 29 = SPI 编号 = IRQ_F2P[0] 对应的 GIC SPI ID(61) - 32 = 29
* 4 = active-high level-sensitive(见 UG585 表 7-4)
*/
uio,name = "pwm_ctrl"; /* /sys/class/uio/uio0/name 里显示 */
};
};
};
interrupts 的 SPI 编号怎么算:
Zynq 的 PL 中断线 IRQ_F2P[N] 对应的 GIC SPI ID 见 UG585 Table 7-4。最常用的几个:
| PL 中断线 | GIC SPI ID | 设备树 SPI 编号(SPI ID - 32) |
|---|---|---|
IRQ_F2P[0] | 61 | 29 |
IRQ_F2P[1] | 62 | 30 |
IRQ_F2P[7] | 68 | 36 |
IRQ_F2P[8] | 84 | 52 |
在 Vivado Block Design 里,PWM IP 的中断输出接到 xlconcat_0 的 In0,再接到 processing_system7_0 的 IRQ_F2P[0:0],所以用 SPI 编号 29。
🚧 避坑:如果你在 Vivado 里把 PWM 中断接到了
IRQ_F2P[1],设备树里就得改成interrupts = <0 30 4>。不要照抄我这里的数字——以你实际 Block Design 的连接为准,在 Vivado 的 Diagram 里找到IRQ_F2P的连线。
3.3 确认设备节点出现
重新 build 并启动后:
# 在板子 Linux 上
ls /dev/uio*
# 期望输出:/dev/uio0
cat /sys/class/uio/uio0/name
# 期望输出:pwm_ctrl
cat /sys/class/uio/uio0/maps/map0/addr
# 期望输出:0x43c00000
cat /sys/class/uio/uio0/maps/map0/size
# 期望输出:0x10000
如果 /dev/uio0 不出现:
dmesg | grep uio— 看 uio_pdrv_genirq 有没有 probedmesg | grep 43c0— 看地址有没有冲突lsmod | grep uio— 如果是模块方式,确认已modprobe uio_pdrv_genirq
4. 完整 C 代码:用户态控制 PWM IP
下面是完整、可编译的用户态程序,控制第 06 篇的 PWM IP 调节占空比,并等待中断。
PWM IP 寄存器定义(来自第 06 篇 AXI Slave 寄存器映射):
| 偏移 | 名称 | 描述 |
|---|---|---|
0x00 | CTRL | [0] enable,[1] reset |
0x04 | PERIOD | PWM 周期(PL 时钟 clock cycles) |
0x08 | HIGH | 高电平时长(clock cycles,即占空比) |
0x0C | STATUS | [0] busy,[1] irq_pending |
/*
* pwm_uio_ctrl.c — 通过 UIO 接口控制 Zynq PL 自定义 PWM IP
*
* 编译(在板子上或交叉编译):
* aarch64-linux-gnu-gcc -O2 -Wall -o pwm_uio_ctrl pwm_uio_ctrl.c
*
* 运行:
* ./pwm_uio_ctrl 75 # 设置占空比 75%
*
* 依赖:
* - /dev/uio0 已出现(设备树节点 compatible = "generic-uio")
* - PL 时钟 FCLK_CLK0 = 100 MHz(在 PS7 配置中设置)
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
/* ── PWM IP 寄存器偏移 ── */
#define PWM_CTRL_REG 0x00 /* [0]=enable [1]=reset */
#define PWM_PERIOD_REG 0x04 /* 周期,单位:PL 时钟 cycle */
#define PWM_HIGH_REG 0x08 /* 高电平时长,单位:PL 时钟 cycle */
#define PWM_STATUS_REG 0x0C /* [0]=busy [1]=irq_pending */
#define UIO_DEV "/dev/uio0"
#define MAP_SIZE 0x10000 /* 64KB,和设备树 reg 大小一致 */
/* ── 寄存器访问宏(volatile 防止编译器优化掉 IO 读写) ── */
#define REG_WR(base, off, val) (*(volatile uint32_t *)((uint8_t *)(base) + (off)) = (val))
#define REG_RD(base, off) (*(volatile uint32_t *)((uint8_t *)(base) + (off)))
static void print_status(void *base)
{
uint32_t ctrl = REG_RD(base, PWM_CTRL_REG);
uint32_t period = REG_RD(base, PWM_PERIOD_REG);
uint32_t high = REG_RD(base, PWM_HIGH_REG);
uint32_t status = REG_RD(base, PWM_STATUS_REG);
printf(" CTRL=0x%08x PERIOD=%u HIGH=%u STATUS=0x%08x\n",
ctrl, period, high, status);
if (period > 0)
printf(" → 实际占空比: %.1f%%\n", 100.0f * high / period);
}
int main(int argc, char *argv[])
{
int fd;
void *base;
uint32_t period_cycles, high_cycles;
uint32_t irq_count;
int duty_pct;
if (argc < 2) {
fprintf(stderr, "Usage: %s <duty_percent 0-100>\n", argv[0]);
return 1;
}
duty_pct = atoi(argv[1]);
if (duty_pct < 0 || duty_pct > 100) {
fprintf(stderr, "Error: duty_percent 必须在 0~100 之间\n");
return 1;
}
/* ── Step 1: 打开 UIO 设备 ── */
fd = open(UIO_DEV, O_RDWR);
if (fd < 0) {
fprintf(stderr, "open %s 失败: %s\n", UIO_DEV, strerror(errno));
fprintf(stderr, "提示: 检查 dmesg | grep uio, 确认驱动已 probe\n");
return 1;
}
/* ── Step 2: mmap 寄存器到用户态 ──
*
* UIO 驱动内部调用 remap_pfn_range 时会自动加上 pgprot_noncached(),
* 保证映射是非缓存(Device Memory)的。
* 这意味着你对 base 的每次读写都直接上 AXI 总线,不会被 CPU cache 拦截。
* offset=0 对应设备树 maps/map0(即 0x43C00000)。
*/
base = mmap(NULL, MAP_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0 * getpagesize()); /* offset = map_index × PAGE_SIZE */
if (base == MAP_FAILED) {
fprintf(stderr, "mmap 失败: %s\n", strerror(errno));
close(fd);
return 1;
}
/* ── Step 3: 配置 PWM ──
*
* PL 时钟 FCLK_CLK0 = 100 MHz(在 Vivado PS7 Clocking 配置中设置)
* 目标频率:1 kHz PWM 输出
* period_cycles = 100_000_000 Hz / 1000 Hz = 100_000 cycles
*/
period_cycles = 100000; /* 1 kHz @ 100 MHz PL 时钟 */
high_cycles = (uint32_t)(period_cycles * duty_pct / 100);
printf("[pwm_uio_ctrl] 配置前寄存器状态:\n");
print_status(base);
/* 先关 PWM,防止写 PERIOD 时出现毛刺 */
REG_WR(base, PWM_CTRL_REG, 0x00);
/* 写周期和高电平时长 */
REG_WR(base, PWM_PERIOD_REG, period_cycles);
REG_WR(base, PWM_HIGH_REG, high_cycles);
/* 启动 PWM */
REG_WR(base, PWM_CTRL_REG, 0x01);
printf("[pwm_uio_ctrl] 配置后寄存器状态:\n");
print_status(base);
printf("[pwm_uio_ctrl] PWM 已启动,占空比 %d%%,频率 1kHz\n", duty_pct);
/* ── Step 4: 等待一次 PWM 周期完成中断 ──
*
* UIO 的中断模型:
* - read("/dev/uio0", &irq_count, 4) 会阻塞,直到 IRQ 发生
* - 返回值是累计中断次数(uint32_t,自驱动 probe 以来)
* - 每次 read 返回后,驱动会自动 enable 下一次中断(uio_pdrv_genirq 行为)
*
* 注意:如果你的 PWM IP 中断线没有实际连接,这里会永久阻塞。
* 加 O_NONBLOCK 可以改为非阻塞,但一般调试时阻塞更方便。
*/
printf("[pwm_uio_ctrl] 等待 PWM 中断(阻塞)...\n");
if (read(fd, &irq_count, sizeof(irq_count)) != sizeof(irq_count)) {
perror("read (等待中断)");
/* 非致命,继续 */
} else {
printf("[pwm_uio_ctrl] 收到中断,累计次数: %u\n", irq_count);
}
/* ── Step 5: 清理 ── */
REG_WR(base, PWM_CTRL_REG, 0x00); /* 停止 PWM */
munmap(base, MAP_SIZE);
close(fd);
return 0;
}
交叉编译(在 x86 Ubuntu 开发机上):
# PetaLinux 2023.2 自带 SDK,先 source 环境
source <petalinux-proj>/components/yocto/buildtools/environment-setup-cortexa9t2hf-neon-xilinx-linux-gnueabi
arm-xilinx-linux-gnueabi-gcc -O2 -Wall -o pwm_uio_ctrl pwm_uio_ctrl.c
# scp 到板子
scp pwm_uio_ctrl root@<board-ip>:/home/root/
运行示例:
root@pynq-z2:~# ./pwm_uio_ctrl 75
[pwm_uio_ctrl] 配置前寄存器状态:
CTRL=0x00000000 PERIOD=0 HIGH=0 STATUS=0x00000000
→ 实际占空比: (period=0, 跳过)
[pwm_uio_ctrl] 配置后寄存器状态:
CTRL=0x00000001 PERIOD=100000 HIGH=75000 STATUS=0x00000000
→ 实际占空比: 75.0%
[pwm_uio_ctrl] PWM 已启动,占空比 75%,频率 1kHz
[pwm_uio_ctrl] 等待 PWM 中断(阻塞)...
[pwm_uio_ctrl] 收到中断,累计次数: 1
5. UIO 里的 Cache 一致性问题
这一节是整篇最需要认真读的部分,原资料完全没讲清楚。
为什么 UIO mmap 不需要手动 flush cache?
Zynq 的 AXI-Lite 寄存器空间是 Device Memory,不是普通的 System RAM。Linux 内核里,UIO 驱动(uio_pdrv_genirq)在执行 mmap 时,会对物理页的内存属性做特殊处理:
/* 来自 kernel/drivers/uio/uio.c */
static int uio_mmap_physical(struct vm_area_struct *vma)
{
...
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* ↑ 这一行是关键:强制非缓存映射 */
return remap_pfn_range(vma, vma->vm_start,
mi->addr >> PAGE_SHIFT,
vma->vm_end - vma->vm_start,
vma->vm_page_prot);
}
pgprot_noncached() 在 ARM 架构下会把页表项的 TEX/C/B 位设置为 Strongly Ordered 或 Device,这意味着:
- CPU 不会把对这块地址的读写放入 L1/L2 Cache
- 每次读写都直接走 AXI 总线到 PL 硬件
- 你不需要也无法用
__flush_dcache_area()或类似函数来同步——因为 cache 根本没参与
对比 DMA 缓冲区(System RAM):
如果你用的是 DMA 缓冲区(比如 PS DDR 里一块给 PL 做 DMA 源的 buffer),情况就完全不一样了——CPU 写 DDR 的数据会先在 L1/L2 cache 里,如果你不 flush,PL 通过 AXI HP 端口看到的还是旧数据。这种情况需要:
- 用
dma_alloc_coherent()(走 HP 端口不经过 cache) - 或手动
dma_sync_single_for_device()在 DMA 传输前 flush
但 AXI-Lite 寄存器空间(0x43C00000 这类地址)不是 System RAM,一开始就被标记为 Device Memory,UIO 里的 pgprot_noncached 只是再确保一遍。
🚧 避坑:如果你自己手写一个
mmap实现(完整内核驱动路线),一定要加pgprot_noncached(),不加的话某些 ARM 处理器可能把寄存器读写 speculatively cache 起来,导致你读到的值是 CPU cache 里的旧值,排查起来极度痛苦。
6. /dev/mem 方法:调试快刀,生产禁器
/dev/mem 是 Linux 内核内置的物理内存访问设备,允许 root 用户 mmap 任意物理地址。调试阶段用来验证寄存器地址正不正确非常方便。
/*
* devmem_peek.c — 用 /dev/mem 读写 PWM IP 寄存器(仅调试用)
*
* 编译:gcc -O0 -o devmem_peek devmem_peek.c
* 运行:sudo ./devmem_peek (需要 root)
*/
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define PWM_BASE 0x43C00000UL
#define MAP_SIZE 0x10000 /* 必须是 PAGE_SIZE(4096) 的整数倍 */
int main(void)
{
int fd;
void *base;
fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) { perror("open /dev/mem"); return 1; }
/*
* mmap 的 offset 参数必须是 PAGE_SIZE(4096)的整数倍。
* 0x43C00000 & ~(PAGE_SIZE-1) = 0x43C00000(已对齐,没问题)。
* 如果你的 IP 地址不是 4K 对齐的(罕见但存在),
* 需要先对齐到页边界,再用指针偏移访问目标地址。
*/
base = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, PWM_BASE);
if (base == MAP_FAILED) { perror("mmap"); return 1; }
/* 读 CTRL 寄存器 */
uint32_t ctrl = *(volatile uint32_t *)((uint8_t *)base + 0x00);
printf("CTRL = 0x%08x\n", ctrl);
/* 写 PERIOD = 100000(1kHz @ 100MHz) */
*(volatile uint32_t *)((uint8_t *)base + 0x04) = 100000;
printf("PERIOD 已写入 100000\n");
munmap(base, MAP_SIZE);
close(fd);
return 0;
}
/dev/mem 的几个实际陷阱:
| 陷阱 | 说明 |
|---|---|
| PAGE_SIZE 对齐 | mmap 的 offset 参数(最后一个参数)必须是 getpagesize()(通常 4096)的倍数,不是的话会 EINVAL |
O_SYNC 标志 | 打开 /dev/mem 时加 O_SYNC,防止读写被合并或重排;缺了可能导致写操作延迟生效 |
| 内核 CONFIG_STRICT_DEVMEM | PetaLinux 2023.2 默认开 CONFIG_STRICT_DEVMEM,限制只有非 RAM 的物理地址(即 MMIO 区域)才能访问——对 PL 寄存器(0x43C00000)是放行的,但你不能用它访问 DDR 物理地址 |
| 没有中断支持 | /dev/mem 只能读写,无法等中断;用轮询 STATUS 寄存器勉强替代,效率极差 |
| 多进程不安全 | 两个进程同时 mmap 同一块地址,没有任何互斥保证,在调试多线程应用时容易踩 |
🚧 避坑:
/dev/mem调试完记得删掉,或者加#ifdef DEBUG保护。我见过有人把/dev/mem方法直接用在产品固件里,结果一旦某个进程崩溃,PWM IP 进入未知状态,硬件失控。
7. 完整内核驱动路线(何时需要)
UIO 满足大部分寄存器读写场景,但以下情况需要写完整内核驱动:
- DMA 引擎:
axidma之类需要在内核里管理 scatter-gather descriptor - 多用户互斥:多个进程抢同一个 IP,需要内核级 mutex 保护
- sysfs / debugfs 接口:暴露 IP 状态给系统监控工具
- 与其他内核子系统集成:比如把 PWM IP 注册成
pwm_chip,让/sys/class/pwm管理它
完整驱动的骨架(以 platform driver 为例):
/* pwm_ip_drv.c — 最小可工作骨架,Vivado 2023.2 / PetaLinux 2023.2 */
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#define DRV_NAME "pwm-ip"
/* 寄存器偏移(和 UIO 版本一致) */
#define PWM_CTRL 0x00
#define PWM_PERIOD 0x04
#define PWM_HIGH 0x08
#define PWM_STATUS 0x0C
struct pwm_ip_dev {
void __iomem *base; /* ioremap 后的内核虚拟地址 */
struct miscdevice mdev;
};
static long pwm_ip_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct pwm_ip_dev *priv = container_of(filp->private_data,
struct pwm_ip_dev, mdev);
/* 实际项目里在这里实现 _IOR/_IOW 命令 */
(void)priv; (void)cmd; (void)arg;
return -ENOTTY;
}
static const struct file_operations pwm_ip_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = pwm_ip_ioctl,
};
static int pwm_ip_probe(struct platform_device *pdev)
{
struct pwm_ip_dev *priv;
struct resource *res;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv) return -ENOMEM;
/* 从设备树 reg 属性获取物理地址和大小 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base)) return PTR_ERR(priv->base);
/* 注册 misc 设备 → /dev/pwm_ip */
priv->mdev.minor = MISC_DYNAMIC_MINOR;
priv->mdev.name = DRV_NAME;
priv->mdev.fops = &pwm_ip_fops;
ret = misc_register(&priv->mdev);
if (ret) return ret;
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "PWM IP @ %pa probed OK\n", &res->start);
return 0;
}
static int pwm_ip_remove(struct platform_device *pdev)
{
struct pwm_ip_dev *priv = platform_get_drvdata(pdev);
misc_deregister(&priv->mdev);
return 0;
}
static const struct of_device_id pwm_ip_of_ids[] = {
{ .compatible = "kaiyo,pwm-ip-1.0" }, /* 和设备树 compatible 一致 */
{ }
};
MODULE_DEVICE_TABLE(of, pwm_ip_of_ids);
static struct platform_driver pwm_ip_driver = {
.probe = pwm_ip_probe,
.remove = pwm_ip_remove,
.driver = {
.name = DRV_NAME,
.of_match_table = pwm_ip_of_ids,
},
};
module_platform_driver(pwm_ip_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kaiyo Nan");
MODULE_DESCRIPTION("Zynq PL PWM IP 内核驱动骨架");
注意:完整驱动的设备树里 compatible 要改成你自己的字符串(如 "kaiyo,pwm-ip-1.0"),不能继续用 "generic-uio",否则 uio_pdrv_genirq 和你的驱动会抢同一个设备,出现 probe 竞争。
🚧 避坑:
devm_ioremap_resource这一行做的事情是:从struct resource读出物理地址和大小,调用request_mem_region登记(防止其他驱动重复映射),再ioremap成内核虚拟地址。不要自己写ioremap(0x43C00000, 0x10000)——硬编码地址会在设备树地址变更时悄悄失效,并且绕过了内核的资源管理。
8. 本篇你应该带走的几个判断
- 能独立从 Vivado Address Editor 读出 PL IP 的物理基地址和范围,并写到设备树
reg里 - 知道
interrupts = <0 29 4>里的 29 是怎么从IRQ_F2P[0]算出来的 - 知道 UIO 的 mmap 用
pgprot_noncached,所以不需要手动 flush cache - 能区分 AXI-Lite 寄存器(Device Memory,天然非缓存)和 DMA buffer(System RAM,需要 cache 管理)
- 知道
/dev/mem的 PAGE_SIZE 对齐要求,以及它不能用于中断处理 - 能判断自己的场景适合 UIO 还是需要写完整内核驱动
9. 下一篇预告
下一篇 《Zynq 实战 12|AXI DMA 引擎驱动:从 PL 向 PS DDR 高速搬数据》,我们会:
- 在 Vivado 里加 AXI DMA IP,配置 MM2S / S2MM 通道
- 用 Linux
dmaengine框架(不是手写驱动)操控 AXIDMA - 测试实际吞吐:HP 端口、burst size、对 DDR 带宽的影响
- 捋清楚
dma_alloc_coherent和dma_map_single的区别——以及在 Zynq HP 端口下各自的正确用法
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| UG585 | Zynq-7000 SoC TRM | 第 7 章 Table 7-4:IRQ_F2P 到 GIC SPI ID 的映射 |
| UG585 | Zynq-7000 SoC TRM | 第 22 章:OCM / DDR 地址映射,以及 AXI GP/HP 端口地址范围 |
| UG1144 | PetaLinux Tools Reference Guide 2023.2 | 第 4 章:device tree 定制,system-user.dtsi 位置 |
| Linux kernel | drivers/uio/uio.c | UIO 框架源码,uio_mmap_physical 函数里可以看到 pgprot_noncached |
| Linux kernel | drivers/uio/uio_pdrv_genirq.c | generic-uio 驱动源码,了解中断处理逻辑 |
| PG021 | AXI GPIO Product Guide | GPIO IP 参考,AXI-Lite 从机寄存器布局示例 |
所有文档均可在 AMD 官方文档页 免费下载。Linux 内核源码在 elixir.bootlin.com 可以在线浏览。
这是《Zynq FPGA 嵌入式系统设计实战》系列第 11 篇。 如果你在 UIO probe、设备树调试、或 cache 一致性问题上踩了别的坑,欢迎留言。