Zynq 实战 10|Linux 设备驱动开发基础:从字符设备骨架到 PWM IP 驱动上板
Zynq 实战 10|Linux 设备驱动开发基础:从字符设备骨架到 PWM IP 驱动上板
这是《Zynq FPGA 嵌入式系统设计实战》系列的第 10 篇。 板子:Pynq-Z2(XC7Z020)。工具链:Vivado / Vitis / PetaLinux 2023.2。内核版本:linux-xlnx 6.1.x。 上一篇:Zynq 实战 09|PetaLinux 根文件系统定制与 rootfs 打包
0. 这一篇要解决什么问题
第 06 篇我们在 PL 里搭了一个 PWM IP,AXI 基地址 0x43C0_0000,有三个寄存器:CTRL、PERIOD、DUTY。现在 PetaLinux 跑起来了,但那三个寄存器只能靠 /dev/mem 裸写——这不是正确姿势,权限不可控、无法多进程安全共用、还绕过了内核的内存管理。
这一篇写一个真正的 Linux 字符设备驱动,让用户态用 open("/dev/zynq-pwm", ...) + ioctl() 控制 PWM,跟普通 Linux 程序一样。
读完能做到:
- 写字符设备驱动骨架,能在 Zynq 上 insmod 跑起来
- 理解 platform_driver 和设备树 compatible 字符串怎么对上
- 把驱动用 bitbake recipe 集成进 PetaLinux rootfs
本文不讲:中断驱动、DMA、UIO 框架——那些各自成篇。
1. Linux 驱动模型:bus / device / driver 三角
Linux 内核用三个对象管理所有硬件:总线(Bus)、设备(Device)、驱动(Driver)。它们的关系:
- Bus 是撮合方,维护两张表:已注册的 device 列表 + 已注册的 driver 列表。每次新增 device 或 driver,Bus 都会用
.match()函数做一次配对 - Device 描述”有什么硬件”——地址范围、中断号、时钟,来自设备树或 ACPI
- Driver 描述”怎么操作这个硬件”——
probe()、remove()、suspend()、resume()
配对成功后,Bus 调用 driver->probe(device),从这一刻起驱动接管设备。
Zynq 上为什么默认用 platform_driver?
PCI/USB 总线有标准的枚举协议,硬件自我描述。但 PL 里的自定义 IP 没有任何自我描述能力——它就是一片挂在 AXI 总线上的寄存器,内核无法自动发现。
Linux 为这类”内存映射寄存器,靠软件描述”的设备设计了 Platform Bus:设备信息在设备树里写死,驱动注册 platform_driver,Bus 通过 compatible 字符串做匹配。Zynq PL 里的一切自定义 IP,第一选择都是 platform_driver。
🚧 避坑:有些教程让你直接
ioremap(0x43C00000, ...)写在module_init()里。这能跑,但是错的。没有走 platform_driver,内核的request_mem_region()没有执行,另一个驱动或/dev/mem可以同时访问同一块物理地址,出现数据竞争。正确方式是用devm_ioremap_resource(),它内部先request_mem_region()占住地址范围,再做 ioremap。
2. 字符设备驱动最小骨架
字符设备(Character Device)以字节流方式访问,用 open/read/write/ioctl 操作。它的内核对象是 struct cdev。
注册一个字符设备的四步:
alloc_chrdev_region() → 分配设备号(主设备号 + 次设备号)
cdev_init() + cdev_add() → 关联 file_operations,注册到内核
class_create() → 在 /sys/class/ 下创建设备类
device_create() → 触发 udev 自动在 /dev/ 下创建节点
卸载时逆序:device_destroy → cdev_del → unregister_chrdev_region → class_destroy。
数据流全景图(用户态 ioctl 到物理寄存器的完整路径):
3. 设备树绑定:compatible 字符串如何撮合驱动和 IP
3.1 设备树节点(system-user.dtsi)
在 PetaLinux 工程里,自定义节点加在 project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi:
// system-user.dtsi
/include/ "system-conf.dtsi"
/ {
};
&amba_pl {
kaiyo_pwm_0: pwm@43c00000 {
compatible = "kaiyo,zynq-pwm-1.0";
reg = <0x43c00000 0x10000>;
/* 无中断——轮询模式,第 11 篇再加中断 */
};
};
几个关键点:
reg = <基地址 大小>,这里0x10000= 64KB,AXI GP0 地址空间按 64KB 对齐是惯例(来自 UG585 第 5 章 Address Map,AXI GP0 对应0x4000_0000 ~ 0x7FFF_FFFF)&amba_pl是 Xilinx BSP 设备树里已经定义好的 Platform Bus 节点,追加子节点即可compatible字符串格式是"厂商名,设备名-版本",与驱动里of_device_id表里的字符串精确匹配
🚧 避坑:compatible 字符串区分大小写,一个字符不同就匹配不上。你在 Vivado 给 IP 起名叫
Kaiyo_PWM,设备树里写"kaiyo,zynq-pwm-1.0",驱动里也必须原字节相同。用dmesg | grep -i "no driver"可以快速定位没匹配上的节点。
3.2 驱动里的 of_device_id 表
static const struct of_device_id pwm_of_match[] = {
{ .compatible = "kaiyo,zynq-pwm-1.0" },
{ /* sentinel - 必须以空项结尾 */ }
};
MODULE_DEVICE_TABLE(of, pwm_of_match);
MODULE_DEVICE_TABLE(of, pwm_of_match) 这一行让 depmod 在生成 modules.alias 时把这条 compatible 记录在案,udev 才能在检测到设备树节点时自动 modprobe 加载驱动。如果你用 insmod 手动加载,这行没有也能跑,但集成进 rootfs 后自动加载就靠它。
4. 完整 PWM IP 驱动源码
4.1 头文件(zynq_pwm_drv.h)
ioctl 命令号用 _IO / _IOW 宏生成,不要自己写整数——不同驱动的命令号可能冲突:
/* zynq_pwm_drv.h
* 用户态和内核态共享的 ioctl 接口定义
* 用户测试程序 #include 这个头文件即可
*/
#ifndef __ZYNQ_PWM_DRV_H__
#define __ZYNQ_PWM_DRV_H__
#include <linux/ioctl.h>
/* PWM IP 寄存器偏移(相对于 AXI 基地址)
* 与第 06 篇 Vivado IP 设计中的寄存器布局对应
*/
#define PWM_CTRL_REG 0x00 /* bit[0]=EN, bit[1]=SW_RST */
#define PWM_PERIOD_REG 0x04 /* 周期(100MHz 时钟周期数,u32)*/
#define PWM_DUTY_REG 0x08 /* 占空比(时钟周期数,必须 < PERIOD)*/
/* ioctl 命令字
* Magic number 'P'(0x50)与其他常见驱动区分
* 范围 0~3,查 Documentation/userspace-api/ioctl/ioctl-number.rst 避开冲突
*/
#define PWM_IOC_MAGIC 'P'
#define PWMIP_SET_PERIOD _IOW(PWM_IOC_MAGIC, 0, unsigned int)
#define PWMIP_SET_DUTY _IOW(PWM_IOC_MAGIC, 1, unsigned int)
#define PWMIP_ENABLE _IO (PWM_IOC_MAGIC, 2)
#define PWMIP_DISABLE _IO (PWM_IOC_MAGIC, 3)
/* 换算辅助宏(用户态方便计算)
* 例:100kHz @ 100MHz 时钟 → period = 1000 cycles
* 50% 占空比 → duty = 500 cycles
*/
#define PWM_HZ_TO_CYCLES(hz) (100000000U / (hz))
#define PWM_DUTY_PCT_TO_CYCLES(period_cycles, pct) \
((unsigned int)((unsigned long long)(period_cycles) * (pct) / 100))
#endif /* __ZYNQ_PWM_DRV_H__ */
4.2 驱动主体(zynq_pwm_drv.c)
这是完整可编译版本,不省略任何 #include 和错误路径:
// SPDX-License-Identifier: GPL-2.0
/*
* zynq_pwm_drv.c — Zynq 自定义 PWM IP 的 platform 字符设备驱动
*
* 适用:PetaLinux 2023.2 / linux-xlnx 6.1.x / Pynq-Z2 (XC7Z020)
* 对应 IP 物理基地址:0x43C0_0000(AXI GP0 地址空间)
* 寄存器布局见 zynq_pwm_drv.h
*
* 作者:Kaiyo Nan
*/
#include <linux/module.h> /* module_init / module_exit / MODULE_* */
#include <linux/platform_device.h> /* platform_driver / platform_get_resource */
#include <linux/of.h> /* of_device_id / MODULE_DEVICE_TABLE */
#include <linux/of_device.h> /* of_match_device */
#include <linux/fs.h> /* file_operations / alloc_chrdev_region */
#include <linux/cdev.h> /* cdev_init / cdev_add / cdev_del */
#include <linux/device.h> /* class_create / device_create */
#include <linux/uaccess.h> /* copy_from_user */
#include <linux/io.h> /* iowrite32 / ioread32 */
#include <linux/slab.h> /* devm_kzalloc */
#include <linux/err.h> /* IS_ERR / PTR_ERR */
#include "zynq_pwm_drv.h"
#define DRIVER_NAME "zynq-pwm"
#define CLASS_NAME "zynq_pwm"
/* 每个设备实例的私有数据 */
struct pwm_priv {
void __iomem *base; /* ioremap 后的虚拟基地址 */
struct cdev cdev;
dev_t devno;
struct device *sysdev; /* device_create 返回的指针,用于 destroy */
};
/* 全局:设备类(/sys/class/zynq_pwm/),整个驱动只有一个 */
static struct class *pwm_class;
/* ------------------------------------------------------------------ */
/* file_operations 实现 */
/* ------------------------------------------------------------------ */
static int pwm_open(struct inode *inode, struct file *filp)
{
struct pwm_priv *priv;
/* container_of 从 cdev 指针反推 pwm_priv 指针 */
priv = container_of(inode->i_cdev, struct pwm_priv, cdev);
filp->private_data = priv;
dev_dbg(priv->sysdev, "opened\n");
return 0;
}
static int pwm_release(struct inode *inode, struct file *filp)
{
return 0;
}
static long pwm_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct pwm_priv *priv = filp->private_data;
unsigned int val;
u32 ctrl;
/* 校验 magic number 和命令序号范围 */
if (_IOC_TYPE(cmd) != PWM_IOC_MAGIC)
return -ENOTTY;
switch (cmd) {
case PWMIP_SET_PERIOD:
if (copy_from_user(&val, (unsigned int __user *)arg, sizeof(val)))
return -EFAULT;
if (val == 0)
return -EINVAL; /* 周期为 0 无意义,IP 内部会产生 undefined 输出 */
iowrite32(val, priv->base + PWM_PERIOD_REG);
dev_dbg(priv->sysdev, "period = %u cycles\n", val);
break;
case PWMIP_SET_DUTY:
if (copy_from_user(&val, (unsigned int __user *)arg, sizeof(val)))
return -EFAULT;
iowrite32(val, priv->base + PWM_DUTY_REG);
dev_dbg(priv->sysdev, "duty = %u cycles\n", val);
break;
case PWMIP_ENABLE:
ctrl = ioread32(priv->base + PWM_CTRL_REG);
iowrite32(ctrl | BIT(0), priv->base + PWM_CTRL_REG);
dev_dbg(priv->sysdev, "enabled\n");
break;
case PWMIP_DISABLE:
ctrl = ioread32(priv->base + PWM_CTRL_REG);
iowrite32(ctrl & ~BIT(0), priv->base + PWM_CTRL_REG);
dev_dbg(priv->sysdev, "disabled\n");
break;
default:
return -ENOTTY;
}
return 0;
}
static const struct file_operations pwm_fops = {
.owner = THIS_MODULE,
.open = pwm_open,
.release = pwm_release,
.unlocked_ioctl = pwm_ioctl,
/* 64 位用户态程序兼容:compat_ioctl 本例不需要(参数是 u32) */
};
/* ------------------------------------------------------------------ */
/* platform_driver probe / remove */
/* ------------------------------------------------------------------ */
static int pwm_probe(struct platform_device *pdev)
{
struct pwm_priv *priv;
struct resource *res;
int ret;
/* 1. 分配私有数据(devm:设备移除时自动释放) */
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 2. 从设备树取 IORESOURCE_MEM(reg 属性) */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "missing 'reg' in device tree node\n");
return -ENODEV;
}
dev_info(&pdev->dev, "mapped region: phys 0x%08llx size 0x%llx\n",
(unsigned long long)res->start,
(unsigned long long)resource_size(res));
/* 3. request_mem_region + ioremap(devm 一步完成,probe 失败自动撤销) */
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base)) {
dev_err(&pdev->dev, "ioremap failed\n");
return PTR_ERR(priv->base);
}
/* 4. 分配字符设备号(动态分配,避免与系统已有设备冲突) */
ret = alloc_chrdev_region(&priv->devno, 0, 1, DRIVER_NAME);
if (ret) {
dev_err(&pdev->dev, "alloc_chrdev_region: %d\n", ret);
return ret; /* devm 自动释放 ioremap 和 priv */
}
/* 5. 初始化并注册 cdev */
cdev_init(&priv->cdev, &pwm_fops);
priv->cdev.owner = THIS_MODULE;
ret = cdev_add(&priv->cdev, priv->devno, 1);
if (ret) {
dev_err(&pdev->dev, "cdev_add: %d\n", ret);
goto err_unreg_chrdev;
}
/* 6. 在 /dev/ 下创建节点(触发 udev 生成 /dev/zynq-pwm) */
priv->sysdev = device_create(pwm_class, &pdev->dev,
priv->devno, NULL, DRIVER_NAME);
if (IS_ERR(priv->sysdev)) {
ret = PTR_ERR(priv->sysdev);
dev_err(&pdev->dev, "device_create: %d\n", ret);
goto err_del_cdev;
}
/* 7. 硬件初始化:先软复位,再清 EN,确保输出低电平 */
iowrite32(BIT(1), priv->base + PWM_CTRL_REG); /* SW_RST=1 */
iowrite32(0, priv->base + PWM_CTRL_REG); /* SW_RST=0, EN=0 */
/* 设置默认 100kHz 50% 占空比(不 enable,等用户态调用 PWMIP_ENABLE) */
iowrite32(1000, priv->base + PWM_PERIOD_REG);
iowrite32(500, priv->base + PWM_DUTY_REG);
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev,
"zynq-pwm probed OK: /dev/%s major=%d minor=%d\n",
DRIVER_NAME, MAJOR(priv->devno), MINOR(priv->devno));
return 0;
err_del_cdev:
cdev_del(&priv->cdev);
err_unreg_chrdev:
unregister_chrdev_region(priv->devno, 1);
return ret;
}
static int pwm_remove(struct platform_device *pdev)
{
struct pwm_priv *priv = platform_get_drvdata(pdev);
/* 先关 PWM 输出,避免卸载驱动后 IO 引脚锁在高电平 */
iowrite32(0, priv->base + PWM_CTRL_REG);
device_destroy(pwm_class, priv->devno);
cdev_del(&priv->cdev);
unregister_chrdev_region(priv->devno, 1);
/* priv 本身、priv->base(ioremap)由 devm 在此之后自动释放 */
dev_info(&pdev->dev, "zynq-pwm removed\n");
return 0;
}
/* ------------------------------------------------------------------ */
/* of_device_id 匹配表 + platform_driver 注册 */
/* ------------------------------------------------------------------ */
static const struct of_device_id pwm_of_match[] = {
{ .compatible = "kaiyo,zynq-pwm-1.0" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, pwm_of_match);
static struct platform_driver pwm_platform_driver = {
.probe = pwm_probe,
.remove = pwm_remove,
.driver = {
.name = DRIVER_NAME,
.of_match_table = pwm_of_match,
.owner = THIS_MODULE,
},
};
/* ------------------------------------------------------------------ */
/* module_init / module_exit */
/* ------------------------------------------------------------------ */
static int __init zynq_pwm_drv_init(void)
{
int ret;
/* class_create 在 linux-xlnx 6.1 中仍是两参数形式
* 注意:上游内核 6.4 起改为单参数,PetaLinux 2023.2 用 6.1,此处正确 */
pwm_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(pwm_class)) {
pr_err(DRIVER_NAME ": class_create failed: %ld\n",
PTR_ERR(pwm_class));
return PTR_ERR(pwm_class);
}
ret = platform_driver_register(&pwm_platform_driver);
if (ret) {
class_destroy(pwm_class);
return ret;
}
pr_info(DRIVER_NAME ": driver registered\n");
return 0;
}
static void __exit zynq_pwm_drv_exit(void)
{
platform_driver_unregister(&pwm_platform_driver);
class_destroy(pwm_class);
pr_info(DRIVER_NAME ": driver unregistered\n");
}
module_init(zynq_pwm_drv_init);
module_exit(zynq_pwm_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kaiyo Nan");
MODULE_DESCRIPTION("Zynq Custom PWM IP platform character driver");
MODULE_VERSION("1.0");
🚧 避坑(API 版本):网上很多教程把
class_create(THIS_MODULE, name)和class_create(name)混用。两个参数的旧接口是 Linux 6.4 之前的写法;PetaLinux 2023.2 使用 linux-xlnx 6.1.5(可在petalinux-config -c kernel→ Kernel Version 查到),编译时用旧接口。如果你拿这份代码去编译 6.6 内核,把那行改成pwm_class = class_create(CLASS_NAME);即可。
5. Makefile 与 PetaLinux .bb Recipe 集成
5.1 独立 Makefile(开发阶段手动交叉编译用)
# Makefile — 用于开发阶段独立交叉编译
# 用法:make KDIR=/path/to/linux-xlnx-build
obj-m := zynq-pwm-drv.o
zynq-pwm-drv-objs := zynq_pwm_drv.o
# KDIR 指向已编译过一次的内核源码树(需要有 Module.symvers)
# PetaLinux 工程里路径通常是:
# <proj>/build/tmp/work/plnx_zynq7-xilinx-linux-gnueabi/linux-xlnx/<version>/build
KDIR ?= $(HOME)/plnx-proj/build/tmp/work/plnx_zynq7-xilinx-linux-gnueabi/linux-xlnx/6.1.5+git999-r0/build
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-gnueabihf-
all:
$(MAKE) -C $(KDIR) M=$(PWD) \
ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) \
modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
.PHONY: all clean
5.2 集成进 PetaLinux rootfs 的 bitbake recipe
文件布局(放在 PetaLinux 工程目录下):
<petalinux-proj>/project-spec/meta-user/
└── recipes-modules/
└── zynq-pwm-drv/
├── zynq-pwm-drv.bb ← bitbake recipe
└── files/
├── zynq_pwm_drv.c
├── zynq_pwm_drv.h
├── Makefile ← 只有两行(见下)
└── COPYING ← GPL-2.0 许可证文本
bitbake recipe(zynq-pwm-drv.bb):
# zynq-pwm-drv.bb
# PetaLinux 2023.2 / Yocto Kirkstone 兼容
SUMMARY = "Zynq custom PWM IP platform character driver"
DESCRIPTION = "Platform driver for kaiyo,zynq-pwm-1.0 IP core on Pynq-Z2"
LICENSE = "GPL-2.0-only"
LIC_FILES_CHKSUM = "file://COPYING;md5=12f884d2ae1ff87c09e5b7ccc2c4ca7e"
inherit module
SRC_URI = "file://zynq_pwm_drv.c \
file://zynq_pwm_drv.h \
file://Makefile \
file://COPYING \
"
S = "${WORKDIR}"
# bitbake 的 module bbclass 会自动:
# 1. 用内核构建系统交叉编译
# 2. 把 .ko 装进 rootfs 的 /lib/modules/<kver>/extra/
# 3. 运行 depmod 更新 modules.dep
# 开机自动加载(可选)
# 如果去掉这行,设备树 compatible 匹配后 udev + modprobe 也会自动加载
KERNEL_MODULE_AUTOLOAD += "zynq-pwm-drv"
# 让 zynq-pwm-drv.h 也能被用户态测试程序引用
# (放到 /usr/include/zynq_pwm_drv.h)
do_install_append() {
install -d ${D}${includedir}
install -m 0644 ${S}/zynq_pwm_drv.h ${D}${includedir}/
}
模块专用 Makefile(recipes-modules/…/files/Makefile):
bitbake 的 module bbclass 会帮你传入 KDIR、ARCH、CROSS_COMPILE,所以这个 Makefile 只需要两行:
obj-m := zynq-pwm-drv.o
zynq-pwm-drv-objs := zynq_pwm_drv.o
注册到 PetaLinux rootfs 包列表,在 petalinux-config -c rootfs → user packages 里勾选 zynq-pwm-drv,然后 petalinux-build 即可。
🚧 避坑(AUTOREV 与离线构建):如果你的
SRC_URI用git://...,必须设SRCREV;但本例用本地file://,不需要 SRCREV,也不依赖网络,离线环境 bitbake 也能跑。新手常犯的错是把SRCREV = "${AUTOREV}"加进去又没有连网,导致 fetch 失败,半小时后才反应过来。
6. insmod / rmmod / dmesg 调试流程
6.1 基本流程
把编译好的 zynq-pwm-drv.ko 和头文件拷到板子上(SCP 或 NFS 挂载):
# 在开发机上
scp zynq-pwm-drv.ko zynq_pwm_drv.h root@192.168.1.100:/root/
# 在 Zynq 板子上
insmod /root/zynq-pwm-drv.ko
dmesg | tail -10
# 期望看到:
# [ 12.345678] zynq-pwm 43c00000.pwm: mapped region: phys 0x43c00000 size 0x10000
# [ 12.346001] zynq-pwm 43c00000.pwm: zynq-pwm probed OK: /dev/zynq-pwm major=240 minor=0
ls -la /dev/zynq-pwm
# crw------- 1 root root 240, 0 ...
# 卸载
rmmod zynq-pwm-drv
dmesg | tail -3
# [ 30.123456] zynq-pwm 43c00000.pwm: zynq-pwm removed
如果 insmod 后没有 probe 打印,90% 是设备树 compatible 没对上:
# 检查内核解析到的设备树节点
cat /sys/firmware/devicetree/base/amba_pl/pwm@43c00000/compatible
# 应该打出:kaiyo,zynq-pwm-1.0
# 检查是否有 driver 绑定上
ls /sys/bus/platform/drivers/zynq-pwm/
# 如果目录存在且有 43c00000.pwm 符号链接,说明匹配成功
6.2 printk 等级与 dmesg 过滤
内核日志按重要性分 8 级,级别数字越小越严重:
| 宏 | 数字 | 含义 | 什么时候用 |
|---|---|---|---|
KERN_EMERG | 0 | 系统即将崩溃 | 不会用 |
KERN_ALERT | 1 | 必须立即处理 | 不会用 |
KERN_CRIT | 2 | 严重故障 | 硬件不可恢复错误 |
KERN_ERR | 3 | 错误 | 函数返回错误、资源分配失败 |
KERN_WARNING | 4 | 警告 | 参数不规范、降级运行 |
KERN_NOTICE | 5 | 注意 | 正常但值得记录的事件 |
KERN_INFO | 6 | 信息 | probe 成功、remove 信息 |
KERN_DEBUG | 7 | 调试 | 每次 ioctl 细节(上线后不要留着) |
实际使用中建议用 dev_info() / dev_err() / dev_dbg() 替代裸 printk,因为前者会自动加上设备路径前缀,多个驱动日志混在一起时容易区分。
dev_dbg() 的打印默认关闭——即使 dmesg 什么都看不到,不代表代码没跑到:
# 动态开启 DEBUG 级别输出(不用重新编译)
echo "module zynq-pwm-drv +p" > /sys/kernel/debug/dynamic_debug/control
# 之后 dmesg 就能看到 dev_dbg() 的输出了
# 也可以指定文件
echo "file zynq_pwm_drv.c +p" > /sys/kernel/debug/dynamic_debug/control
6.3 /dev 节点权限与 udev 规则
默认 device_create() 生成的节点是 root:root 600,普通用户没有读写权限。调试阶段可以临时改权限:
chmod 666 /dev/zynq-pwm # 临时,重启后失效
要永久生效,在 project-spec/meta-user/recipes-bsp/device-tree/files/ 旁边添加 udev 规则,或者在 rootfs 里加一条规则文件:
99-zynq-pwm.rules(放到 rootfs 的 /etc/udev/rules.d/):
# 按 subsystem 匹配(CLASS_NAME = "zynq_pwm")
SUBSYSTEM=="zynq_pwm", MODE="0666", GROUP="users"
# 或者按设备名精确匹配
KERNEL=="zynq-pwm", MODE="0666"
在 PetaLinux 里通过 recipes-core/packagegroups 或直接在 rootfs/etc/udev/rules.d/ 下加文件,petalinux-build 会自动打包进镜像。
7. 用户态测试程序(最小验证)
/* test_pwm.c — 用于验证驱动 ioctl 接口
* 编译:arm-linux-gnueabihf-gcc -o test_pwm test_pwm.c
* 运行:./test_pwm (板子上执行)
*/
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "zynq_pwm_drv.h"
int main(void)
{
int fd;
unsigned int period, duty;
fd = open("/dev/zynq-pwm", O_RDWR);
if (fd < 0) {
perror("open /dev/zynq-pwm");
return 1;
}
/* 50kHz,50% 占空比 */
period = PWM_HZ_TO_CYCLES(50000); /* = 2000 */
duty = PWM_DUTY_PCT_TO_CYCLES(period, 50); /* = 1000 */
if (ioctl(fd, PWMIP_SET_PERIOD, &period) < 0) { perror("SET_PERIOD"); goto out; }
if (ioctl(fd, PWMIP_SET_DUTY, &duty) < 0) { perror("SET_DUTY"); goto out; }
if (ioctl(fd, PWMIP_ENABLE) < 0) { perror("ENABLE"); goto out; }
printf("PWM enabled: %u Hz, 50%% duty\n", 50000);
printf("Press Enter to disable...\n");
getchar();
ioctl(fd, PWMIP_DISABLE);
printf("PWM disabled\n");
out:
close(fd);
return 0;
}
8. 本篇你应该带走的几个判断
-
platform_driver是 Zynq PL 自定义 IP 的默认驱动框架,原因是 PL IP 无法自我枚举,只能靠设备树描述 -
devm_ioremap_resource()不只是 ioremap——它先request_mem_region()占住地址,防止两个驱动同时访问同一物理地址 -
compatible字符串区分大小写,驱动的of_device_id表和设备树节点必须字节相同 -
class_create(THIS_MODULE, name)是 Linux 6.1(PetaLinux 2023.2 内核版本)的正确 API;6.4 起改为单参数 -
dev_dbg()默认不输出,调试时用 dynamic debug 开关,不要靠 dev_dbg 消失来判断代码没运行 - udev 规则通过
SUBSYSTEM或KERNEL匹配设备节点,不写规则默认root:root 600
9. 下一篇预告
下一篇 《Zynq 实战 11|驱动中断处理:从 GIC 到 request_irq》,我们会:
- 把这个 PWM 驱动改造成中断驱动——PWM 每个周期结束时产生一个中断,驱动里
request_irq()注册 handler - 设备树里
interrupts属性怎么写(PPI vs SPI,Zynq 上 PL 中断接到 GIC SPI 口,中断号 = 61 + offset) - 中断 handler 里什么能做、什么不能做(不能睡眠、不能调用
copy_to_user) - 用
wait_queue实现用户态阻塞读等中断
参考资料
| 文档号 / 来源 | 名称 | 用途 |
|---|---|---|
| UG585 Ch. 5 | Zynq-7000 SoC TRM — Interconnect | AXI GP0 地址映射 0x4000_0000~0x7FFF_FFFF |
| UG585 Ch. 29 | Zynq-7000 SoC TRM — PS I/O Peripherals MIO | MIO 与 EMIO 引脚分配 |
| UG1144 | PetaLinux Tools Reference Guide 2023.2 | recipes-modules 集成、bitbake 命令 |
| kernel.org | Linux Device Drivers (LDD3) Ch. 3 | 字符设备驱动经典参考 |
| kernel.org | Documentation/driver-api/driver-model/platform.rst | platform_driver 官方文档 |
| kernel.org | Documentation/admin-guide/dynamic-debug-howto.rst | dynamic debug 使用方法 |
| kernel.org | Documentation/userspace-api/ioctl/ioctl-number.rst | ioctl magic number 分配表 |
所有 Xilinx 文档可在 AMD 文档中心 免费下载。