← Back to Blog
FPGAZynqLinuxLinux Driverplatform driver字符设备设备树PetaLinuxioctlioremap

Zynq 实战 10|Linux 设备驱动开发基础:从字符设备骨架到 PWM IP 驱动上板

This article was written in Chinese and auto-translated via Google Translate.
View Chinese Original →

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 到物理寄存器的完整路径):

ioctl() 数据流:用户态 → 物理寄存器 用户态(User Space) 用户程序 ioctl(fd, PWMIP_SET_PERIOD, &val) /dev/zynq-pwm chr 主:次 = 240:0 ─── syscall 陷入内核 ─── 内核态(Kernel Space) VFS 层 sys_ioctl() → vfs_ioctl() file_operations .unlocked_ioctl = pwm_ioctl(filp, cmd, arg) pwm_ioctl() 内核函数 copy_from_user() → iowrite32(val, base+0x04) ioremap 虚拟地址(内核 vmalloc 空间) devm_ioremap_resource() 映射 ARM MMU 虚拟→物理地址转换 PWM IP 寄存器 物理地址 0x43C00004 ↕ AXI GP0 总线 PS ↔ PL 互联
图 1. ioctl() 调用链:用户态 → /dev 节点 → file_operations → ioremap → 物理寄存器

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 会帮你传入 KDIRARCHCROSS_COMPILE,所以这个 Makefile 只需要两行:

obj-m          := zynq-pwm-drv.o
zynq-pwm-drv-objs := zynq_pwm_drv.o

注册到 PetaLinux rootfs 包列表,在 petalinux-config -c rootfsuser packages 里勾选 zynq-pwm-drv,然后 petalinux-build 即可。

🚧 避坑(AUTOREV 与离线构建):如果你的 SRC_URIgit://...,必须设 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_EMERG0系统即将崩溃不会用
KERN_ALERT1必须立即处理不会用
KERN_CRIT2严重故障硬件不可恢复错误
KERN_ERR3错误函数返回错误、资源分配失败
KERN_WARNING4警告参数不规范、降级运行
KERN_NOTICE5注意正常但值得记录的事件
KERN_INFO6信息probe 成功、remove 信息
KERN_DEBUG7调试每次 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 规则通过 SUBSYSTEMKERNEL 匹配设备节点,不写规则默认 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. 5Zynq-7000 SoC TRM — InterconnectAXI GP0 地址映射 0x4000_0000~0x7FFF_FFFF
UG585 Ch. 29Zynq-7000 SoC TRM — PS I/O Peripherals MIOMIO 与 EMIO 引脚分配
UG1144PetaLinux Tools Reference Guide 2023.2recipes-modules 集成、bitbake 命令
kernel.orgLinux Device Drivers (LDD3) Ch. 3字符设备驱动经典参考
kernel.orgDocumentation/driver-api/driver-model/platform.rstplatform_driver 官方文档
kernel.orgDocumentation/admin-guide/dynamic-debug-howto.rstdynamic debug 使用方法
kernel.orgDocumentation/userspace-api/ioctl/ioctl-number.rstioctl magic number 分配表

所有 Xilinx 文档可在 AMD 文档中心 免费下载。