开发背景
- 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");
本文地址: ZYNQ 7020 PS-PL通信驱动开发