开发背景

  • PL端向寄存器写入数据,并通过共享中断通知PS端接收数据;
  • PS端收到共享中断,从寄存器中读出数据。

环境信息

  • windows11 22H2 + vivado 2018.2
  • ubuntu16.04 + petalinux 2018.2

PL端

PL端需要加入BRAM和共享中断。BRAM作为PL和PS端的公共读写寄存器,充当缓冲区;共享中断用于通知PS端有数据在缓冲区中等待读出。

AX7021B拥有16个共享中断,由于PL端以太网也使用共享中断,因此通过一个Concat核将多个中断信号并行转为串行,PL设计文件(共享中断)如下图所示。

图中第0-11位中断为以太网口和DMA使用的中断,第12位中断为我们通知PS端有数据等待读出的中断, 即第13个共享中断。

PS端

固化Linux操作系统

按照AX7021B开发板的教程手册,使用petalinux工具,在前述生成的.hdf文件上配置编译linux系统,流程中涉及设备树修改的部分请见下一节。按照流程将最终生成的BOOT.BIN文件烧录到QSPI Flash中。

设备树

设备树需要添加一个中断节点,以便PS端可以在驱动中注册中断,使用该中断。需要在设备树dtsi文件中添加如下图所示节点信息。

interrupt-parent属性设置了该节点的中断父,interrupts属性中的三个数据依次表示:0——共享中断(设置为1表示为私有中断),56——中断号,1——上升沿触发。

下表为AX7021芯片datasheet中的共享外部中断表,由表可知16个PL共享中断的中断号分别为61~68和84~91。我们使用的是第13个共享中断,对应的中断号为88。在设备树中设置中断号时,要在PL端中断号上减去32(后面解释),因此设置为56。此外,由表可知共享中断仅支持上升沿或高电平触发,一般电平触发类型的中断对应的是持续性事件。在本例中,数据通知不属于持续性事件,我们选择上升沿触发。不同触发类型定义在linux内核源码include/linux/irq.h中,详见后图。

通信驱动开发

待实现的功能为:用户态程序打开设备后持续调用read函数,以阻塞方式读取缓冲区数据,当缓冲区有数据后返回给用户态程序。

具体驱动的开发使用platform驱动框架,应用了I/O内存访问、中断机制和阻塞IO机制。由于PL端没有实现逻辑,因此中断触发采用按键形式,BRAM中的数据预埋(驱动代码中有体现)。

platform驱动框架

  • 驱动框架

linux 2.6以后的设备驱动模型最重要的3个实体为:总线、设备、驱动。总线负责将设备和驱动匹配。

platform是一种虚拟的总线,其对应的设备为platform_device,驱动为platform_driver。

//主结构platform_driver
static struct platform_driver xxx_driver
{
    .probe = xxx_probe,     //probe()是设备匹配后,声明驱动程序时所调用的函数
    .remove = xxx_remove,   //remove()是驱动程序在被删除时调用
    .driver = {             //描述驱动自身,如名称、所有者等
        .name = "xxx",
        .owner = THIS_MODULE,
    },
};

注意:probe函数和init函数的区别:每当给定设备与驱动程序匹配时,都会调用probe函数,而init函数只在模块加载时运行一次。

向内核注册平台驱动的基本框架:

static int xxx_probe(struct platform_device *pdev)
{
    ...
}

static int xxx_remove(struct platform_device *pdev)
{
    ...
}
static struct platform_driver xxx_driver
{
    .probe = xxx_probe,
    .remove = xxx_remove,
    .driver = {
        ...   
    },
};

module_platform_driver(xxx_driver); //将模块注册到平台驱动程序核心
  • 平台设备

有两种方法可以把有关设备所需的资源(中断、DMA、内存区域、I/O端口、总线)和数据通知内核。一种是内核不支持设备树,另一种是内核支持设备树。其中内核不支持设备树,自行声明平台设备的方法已经废弃。

  • 设备和驱动的匹配

总线驱动程序负责设备和驱动程序的匹配。每当连接新设备或向总线添加新的驱动程序时,总线都会启动匹配循环。

内核中负责平台设备和驱动程序匹配功能的函数platform_match定义在/drivers/base/platform.c中,具体如下:

static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* When driver_override is set, only bind to the matching driver */
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    /* Attempt an OF style match first */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* Then try ACPI style match */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* Then try to match against the id table */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* fall-back to driver name match */
    return (strcmp(pdev->name, drv->name) == 0);
}

由代码可知,内核中共提供4种风格的匹配:OF风格、基于ACPI表、基于ID表、基于平台设备名字。这四种方式都是基于字符串比较。

I/O内存访问

地址空间分为内存空间和I/O空间,X86处理器内存空间和I/O空间采用独立编址,arm处理器内存空间和I/O空间采用统一编址。对设备的控制,通常通过设备的寄存器(控制寄存器、读写寄存器、数据寄存器)来控制、读写设备,当这些寄存器位于I/O空间中,通常被称为I/O端口,当位于内存空间时,通常被称为I/O内存。

在vivado工程中可以查看BRAM的地址范围,可以通过访问I/O内存的方式来读取BRAM中的数据。本例中I/O内存访问的实现机制是将PL端BRAM的物理地址进行映射到一个虚拟地址,通过虚拟地址对该块内存进行读写操作。

内核中提供了ioremap()来映射物理地址到虚拟地址,并使用iounmap()来释放获得的虚拟地址。映射后,可以通过虚拟地址直接读写BRAM。本例中使用的为ioremap_nocache(),为ioremap的无缓存版本,但实际上,在arm体系中,ioremap和ioremap_nocache实现是相同的。本例内核源码中,两者的定义如下图:


中断机制

  • 申请中断号

Linux驱动开发中,使用中断首先要申请中断号。前面我们已经将PL的外部中断写在了设备树中,使用platform_get_irq可以获取dts中设置的中断号。该函数定义在.../drivers/bash/platform.c中,具体代码见下图:

由源码追溯该函数的调用栈:platform_get_irq() ---> of_irq_get() ---> of_irq_parse_one() ,irq_create_of_mapping() ---> of_phandle_args_to_fwspec(), irq_create_fwspec_mapping() ---> irq_domain_translate() ---> translate(),到此,可以看到translate定义在irq_domain_ops中,其具体由某个版本的irq-gic实现。

(GIC,Generic Interrupt Controller)是arm公司提供的一个通用的中断控制器。gic的核心功能是对soc中外设的中断源的管理,并且提供给软件,配置以及控制这些中断源。

在本例使用的内核源码中搜索一下,可以看到以下几个版本,

通过查阅资料,得到ZYNQ的中断控制器是基于GIC V1的版本,即irq-gic.c。打开irq-gic.c,找到translate的具体实现为gic_irq_domain_translate()函数,其具体实现如下:

可以看到,其在dts文件中获取的中断号要先加上16来跳过SGI型(0-15)中断,由于我们使用的是SPI中断,所以需要再加16(在ZYNQ手册中,实际是为了跳过16个PPI型(16-31)中断,跳到SPI型中断起点),即需要在dts文件中获取的中断号上加32,才为对应ZYNQ的中断号。这也解释了我们在dts中设置的中断号,为什么要在ZYNQ手册中断号的基础上减去32。

  • 为中断号申请内核线程

本例中使用request_threaded_irq来为我们申请的中断号分配一个对应的内核线程,该内核线程在中断发生时执行。我们可以在对应的函数中写入中断发生时我们需要进行的一些操作。

阻塞IO机制(等待队列)

当用户态程序使用read、write系统调用时,如果设备的资源不可获取,用户又希望以阻塞的方式访问设备,那么驱动程序应在设备驱动的xxx_read()、xxx_write()等操作中将进程(用户态)阻塞直到资源可以获取。在Linux驱动程序中,可以使用等待队列来实现阻塞进程的唤醒。本驱动中,只要求一个阻塞读函数,其具体的实现原理如下:

struct xxx_device {
    wait_queue_head_t my_queue; //第一步:定义一个等待队列头部
};
struct xxx_device demo_device;

static ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
    int ret;
    DECLARE_WAITQUEUE(wait, current);   //第三步:定义并初始一个名为wait的等待队列元素

    mutex_lock(&demo_device.mutex);
    add_wait_queue(&demo_device.r_wait, &wait); //第四步:将wait元素添加到r_wait等待队列中

    while(demo_device.data_len == 0)
    {
        __set_current_state(TASK_INTERRUPTIBLE);    //改变进程状态,wake_up_interruptible()可以唤醒牌TASK_INTERRUPTIBLE的进程
        mutex_unlock(&demo_device.mutex);

        schedule(); //调度其它进程执行
        if(signal_pending(current)) //如果是因为信号唤醒
        {
            ret = -ERESTARTSYS;
            goto out2;
        }
        mutex_lock(&demo_device.mutex);
    }

    if(copy_to_user(buf, demo_device.reg_bram_base, 4))
    {
        ret = -EFAULT;
        goto out;
    }
    else
    {
        ret = 4;
        demo_device.data_len = 0;
    }
out:
    mutex_unlock(&demo_device.mutex);
out2:
    remove_wait_queue(&demo_device.r_wait, &wait);//将元素移出r_wait等待队列
    set_current_state(TASK_RUNNING);    //设置进程状态为TASK_RUNNING

    return ret;
}

static int xxx_probe(struct platform_device *pdev) {
    ...
    init_waitqueue_head(&demo_device.my_queue); //第二步:初始化等待队列头部
    ...
}

static irqreturn_t xxx_interrupt(int irq, void *dev_id) {
    ...
    demo_device.data_len = 4;
    wake_up_interruptible(&demo_device.r_wait); //该中断由缓冲区可读触发,在中断中唤醒队列。该操作
                                                //会唤醒以r_wait作为等待队列头部的队列中所有的进程。
    ...
}

当用户态程序调用read()时,驱动程序中xxx_read()开始执行,执行到schedule()时,进程进入休眠状态,等待中断里wake_up_interruptible唤醒。唤醒后,将数据从内核空间拷贝到用户空间。这样完成了一次阻塞IO的读写。

完整驱动代码

#include <linux/kernel.h>
#include <linux/version.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/fcntl.h>
#include <linux/mm.h>
#include <linux/proc_fs.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <asm/uaccess.h>
#include <linux/delay.h>
#include <linux/sched/signal.h>

#include <asm/io.h>
#include <linux/miscdevice.h>
#include <linux/proc_fs.h>
#include <linux/poll.h>

#include <asm/bitops.h>
#include <asm/uaccess.h>
#include <asm/irq.h>

#include <linux/moduleparam.h>
#include <linux/ioport.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>

#define DEVICE_NAME "irq_drv"

#define BRAM_BASE 0x80000000
#define BRAM_SIZE 0x1000

#define  ssp_readw(addr,ret)            (ret =(*(volatile unsigned int *)(addr)))
#define  ssp_writew(addr,value)         ((*(volatile unsigned int *)(addr)) = (value))

struct pspl_io_irq_device 
{
    char devname[16];
    int major, minor;
    int irq;
    int irq_is_open;
    int data_len;

    struct class *cls;
    struct device *dev;
    struct fasync_struct *irq_async;
    resource_size_t remap_size;

    void __iomem *reg_bram_base;

    wait_queue_head_t r_wait;
    struct mutex mutex;
};

struct pspl_io_irq_device pspl;

static int pspl_open(struct inode *Inode, struct file *filp)
{
    pspl.irq_is_open = 1;
    filp->private_data = &pspl;

    return 0;
}

int pspl_release(struct inode *inode, struct file *filp)
{
    pspl.irq_is_open = 0;

    return 0;
}

static ssize_t pspl_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
    int ret;
    DECLARE_WAITQUEUE(wait, current);

    mutex_lock(&pspl.mutex);
    add_wait_queue(&pspl.r_wait, &wait);

    while(pspl.data_len == 0)
    {
        __set_current_state(TASK_INTERRUPTIBLE);
        mutex_unlock(&pspl.mutex);

        schedule();
        if(signal_pending(current))
        {
            ret = -ERESTARTSYS;
            goto out2;
        }
        mutex_lock(&pspl.mutex);
    }

    if(copy_to_user(buf, pspl.reg_bram_base, 4))
    {
        ret = -EFAULT;
        goto out;
    }
    else
    {
        ret = 4;
        pspl.data_len = 0;
    }
out:
    mutex_unlock(&pspl.mutex);
out2:
    remove_wait_queue(&pspl.r_wait, &wait);
    set_current_state(TASK_RUNNING);

    return ret;
}

static ssize_t pspl_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    return 0;
}

static int pspl_fasync(int fd, struct file *filp, int on)
{
    return fasync_helper (fd, filp, on, &pspl.irq_async);
}

static struct file_operations pspl_fops = {
    .owner = THIS_MODULE,
    .open = pspl_open,
    .read = pspl_read,
    .write = pspl_write,
    .fasync = pspl_fasync,
    .release = pspl_release,
};

static irqreturn_t pspl_interrupt(int irq, void *dev_id)
{
    printk("irq = %d\n", pspl.irq);
    if(pspl.irq_is_open)
    {
        pspl.data_len = 4;
#if 1
        ssp_writew(pspl.reg_bram_base, 0x11223344);
        //ssp_writew(pspl.reg_bram_base+4, 0xaabbccdd);
        //ssp_writew(pspl.reg_bram_base+8, 0xffffffff);
        //ssp_writew(pspl.reg_bram_base+12, 0xabcdefab);
#endif
        wake_up_interruptible(&pspl.r_wait);
        kill_fasync(&pspl.irq_async, SIGIO, POLL_IN);
    }

    return IRQ_HANDLED;
}

static int pspl_probe(struct platform_device *pdev)
{ 
    int err;
    struct device *tmp_dev;

    printk("irq_probe - sigmapoet\n");

    memset(pspl.devname,0,16);
    strcpy(pspl.devname, DEVICE_NAME);

    pspl.irq_is_open = 0;
    pspl.major = register_chrdev(0, pspl.devname, &pspl_fops);
    pspl.cls = class_create(THIS_MODULE, pspl.devname);
    pspl.minor = 1;
    tmp_dev = device_create(pspl.cls, &pdev->dev, MKDEV(pspl.major, pspl.minor), NULL, pspl.devname);
    if (IS_ERR(tmp_dev)) {
        //printk("irq_probe - device_create error\n");
        class_destroy(pspl.cls);
        unregister_chrdev(pspl.major, pspl.devname);
        goto fail;
    }
    //printk("irq_probe - device_create done\n");

    pspl.irq = platform_get_irq(pdev,0);
    if (pspl.irq <= 0)
    {
        //printk("irq_probe - platform_get_irq error : %d\n", irq);
        return -ENXIO;
    }
    //printk("irq_probe - platform_get_irq done\n");

    pspl.dev = &pdev->dev;
    err = request_threaded_irq(pspl.irq, NULL, pspl_interrupt, IRQF_TRIGGER_RISING | IRQF_ONESHOT, pspl.devname, NULL);
    if (err) {
        printk(KERN_ALERT "irq_probe irqerror=%d\n", err);
        goto fail;
    }
    else
    {
        printk("irq = %d\n", pspl.irq);
        printk("devname = %s\n", pspl.devname);
    }

    init_waitqueue_head(&pspl.r_wait);
    pspl.reg_bram_base = ioremap_nocache((unsigned long)BRAM_BASE, (unsigned long)BRAM_SIZE);
    //保存dev
    //platform_set_drvdata(pdev, &xxx);

    return 0;

fail:
    iounmap((void*)pspl.reg_bram_base);
    free_irq(pspl.irq, NULL);
    device_destroy(pspl.cls, MKDEV(pspl.major, pspl.minor));
    class_destroy(pspl.cls);
    unregister_chrdev(pspl.major, pspl.devname);
    return -ENOMEM;
}

static int pspl_remove(struct platform_device *pdev)
{
    iounmap((void*)pspl.reg_bram_base);
    device_destroy(pspl.cls, MKDEV(pspl.major, pspl.minor));
    class_destroy(pspl.cls);
    unregister_chrdev(pspl.major, pspl.devname);
    free_irq(pspl.irq, NULL);
    //printk("irq = %d\n", irq);
    return 0;
}

static int pspl_suspend(struct device *dev)
{
    return 0;
}

static int pspl_resume(struct device *dev)
{
    return 0;
}

static const struct dev_pm_ops pspl_pm_ops = {
    .suspend = pspl_suspend,
    .resume = pspl_resume,
};

static const struct of_device_id pspl_of_match[] = {
    {.compatible = "hello,irq" },
    { }
};

MODULE_DEVICE_TABLE(of, pspl_of_match);

static struct platform_driver pspl_driver = {
    .probe = pspl_probe,
    .remove = pspl_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "irq@0",
        .pm = &pspl_pm_ops,
        .of_match_table = pspl_of_match,
    },
};

module_platform_driver(pspl_driver);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("sigmapoet");
说点什么
请文明发言!
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...