Zynq用AXI-GPメモリマップドI/Oと割り込みハンドラの実装 (1)

##経緯
UIOを使用してzynqPL-PSの通信と割り込みを行っていましたが，割り込み応答が遅かった(https://qiita.com/tvrcw/items/23f6e0955bbc6dea79d1 )ので割り込みハンドラを実装しました。メモリマップドI/Oの実装をUIOに任せてデバイスツリーを汚す(後述)のも嫌なので，mmapの実装も合わせて行いました。

```axi_gp.c
#include <linux/cdev.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/dma-mapping.h>
#include <linux/fs.h>
#include <linux/idr.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/of_platform.h>
#include <linux/pagemap.h>
#include <linux/platform_device.h>
#include <linux/proc_fs.h>
#include <linux/scatterlist.h>
#include <linux/sched.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/sysctl.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/uio.h>
#include <linux/version.h>

#include <asm/byteorder.h>
#include <asm/page.h>

#define DRIVER_NAME "AXI_GP_DRIVER"
#define DEVICE_NAME "AXI_GP_PL"
#define DEVICE_BASE	"axi_gp"

#define MINOR_BASE 0
#define MINOR_NUM 1

struct resource *r_mem;
struct resource *irq_res;
static int irq;
static unsigned long remap_size;

dev_t dev;
int minor;
unsigned long long irq_cnt;

struct mymodule_local{
	/* Manager for device driver */
	struct cdev c_dev;
	unsigned int mydev_major;
	struct class  *mydev_class;
	/* Information of devices */
	unsigned long mem_start;
	unsigned long mem_end;
};

struct mymodule_local *lp;


#ifndef CONFIG_MMU
static inline int private_mapping_ok(struct vm_area_struct *vma)
{
	return vma->vm_flags & VM_MAYSHARE;
}
#else
static inline int private_mapping_ok(struct vm_area_struct *vma)
{
	return 1;
}
#endif

/*-----------------*/
/* File operations */
/*-----------------*/

static int module_open(struct inode *inode, struct file *file)
{
	return 0;
}

static int module_close(struct inode *inode, struct file *file)
{
	return 0;
}

static ssize_t module_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
  printk("irq_count=%lld, count reset\n",irq_cnt);
	irq_cnt=0;
	return 0;
}

static ssize_t module_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	printk("irq_count=%lld, count reset\n",irq_cnt);
	irq_cnt=0;
	return count;
}

/* mmap implementation */
static struct vm_operations_struct simple_remap_vm_ops={
};

static int module_mmap(struct file *file,struct vm_area_struct *vma){

	size_t size=vma->vm_end-vma->vm_start;

	phys_addr_t offset=(phys_addr_t) lp->mem_start;
	unsigned long pgoff= lp->mem_start>>PAGE_SHIFT;

	if(offset+(phys_addr_t)size-1<offset) return -EINVAL;
	if(!private_mapping_ok(vma)) return -ENOSYS;
	vma->vm_page_prot=phys_mem_access_prot(file,pgoff,size,vma->vm_page_prot);

	vma->vm_ops=&simple_remap_vm_ops;
	if(remap_pfn_range(vma, vma->vm_start, pgoff,size, vma->vm_page_prot)) return -EAGAIN;
	return 0;
}

struct file_operations module_fops = {
    .open    = module_open,
    .release = module_close,
	.read	 = module_read,
	.write	 = module_write,
    .mmap	 = module_mmap,
};

/*--------------------*/
/* Device reistration */
/*--------------------*/

static int module_create_cdev(struct mymodule_local *lp)
{

	minor=0;

	if(alloc_chrdev_region(&dev,MINOR_BASE,MINOR_NUM,DRIVER_NAME)<0){
		printk(KERN_ERR "alloc_chrdev_region failed\n");
		return -1;
	};

	lp->mydev_major=MAJOR(dev);
	cdev_init(&lp->c_dev,&module_fops);
	lp->c_dev.owner=THIS_MODULE;

	if(cdev_add(&lp->c_dev,dev,MINOR_NUM)<0){
		pr_err("cdev_add failed\n");
		return -1;
	}

	lp->mydev_class=class_create(THIS_MODULE,"axi_gp");
	if(IS_ERR(lp->mydev_class)){
		printk(KERN_ERR "class create failed");
		cdev_del(&lp->c_dev);
		unregister_chrdev_region(dev, MINOR_NUM);
		return -1;
	}

	for(minor=0;minor<MINOR_NUM;minor++){
		device_create(lp->mydev_class,NULL,MKDEV(lp->mydev_major,minor),NULL,"%s%d",DEVICE_BASE,minor);
	}

	printk(KERN_INFO "major no.=%d, minor no.=%d\n",MAJOR(dev),MINOR(dev));
    return 0;

}


static void module_delete_cdev(struct mymodule_local *lp)
{
    minor = 0;
    dev = MKDEV(lp->mydev_major,MINOR_BASE);

    for (minor=MINOR_BASE; minor<MINOR_BASE+MINOR_NUM; minor++){
        device_destroy(lp->mydev_class, MKDEV(lp->mydev_major, minor));
    }

    class_destroy(lp->mydev_class);
    cdev_del(&lp->c_dev);
    unregister_chrdev_region(dev, MINOR_NUM);
}


/*-------------*/
/* IRQ handler */
/*-------------*/

//Called function
static irqreturn_t irq_handler(int irq, void *dev_id)
{
	irq_cnt++;
	return IRQ_HANDLED;
}

/*---------------------*/
/* Install & Uninstall */
/*---------------------*/

//prove process
static int module_probe(struct platform_device *pdev)
{
	irq_cnt=0;

	printk(KERN_ALERT "Checking the module %s...\n",DRIVER_NAME);

	r_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if(!r_mem){
		dev_err(&pdev->dev, "cant get mem resource:Invalid address\n");
		return -ENODEV;		/*errno-base.h: No such device (19)*/
	}
	remap_size = r_mem->end - r_mem->start + 1;

	//Check devicetree and get information <GIC channel and IRQ type>
	irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
	if(!irq_res){
		dev_err(&pdev->dev, "cant get irq resource\n");
		return -ENODEV;		/*errno-base.h: No such device (19)*/
	}

	irq = irq_res->start;
	if(irq < 0){
		dev_err(&pdev->dev, "cant get irq resource\n");
		return -ENODEV;		/*errno-base.h: No such device (19)*/
	}

	//Registrate Interrupt Service Routine (ISR)
	if(request_irq(irq, irq_handler, IRQF_TRIGGER_RISING, DEVICE_NAME, NULL)){
		printk(KERN_ERR"fail to request irq %d\n", irq);
		return -EIO;		/*errno-base.h: I/O error (5)*/
	}
	else{
		printk(KERN_INFO"registered IRQ %d\n", irq);
	}

	lp=(struct mymodule_local *) kmalloc(sizeof(struct mymodule_local), GFP_KERNEL);
	if(!lp){
		dev_err(&pdev->dev,"Couldn't allocate module device %p\n",(void *)lp->mem_start);
		return -ENOMEM;
	}

    dev_set_drvdata(&pdev->dev, lp);
    lp->mem_start = r_mem->start;
    lp->mem_end = r_mem->end;

    if (!request_mem_region(lp->mem_start,lp->mem_end-lp->mem_start+1,DRIVER_NAME)){
        dev_err(&pdev->dev, "Couldn't lock memory region at %p\n",(void *)lp->mem_start);
    	kfree(lp);
		dev_set_drvdata(&pdev->dev, NULL);
    	return -EBUSY;
    }

    if(module_create_cdev(lp)){
		release_mem_region(lp->mem_start, lp->mem_end - lp->mem_start + 1);
        return -EIO;
	}

	printk(KERN_ALERT "The module %s was successfully loaded\n",DRIVER_NAME);
	printk(KERN_INFO"name:%s\n", r_mem->name);
	printk(KERN_INFO"start:0x%08lx, size:0x%08lx\n",(unsigned long)r_mem->start,(unsigned long)remap_size);

	return 0;
}

//Remove process
static int module_remove(struct platform_device *pdev)
{
	free_irq(irq, NULL);
	lp=dev_get_drvdata(&pdev->dev);
	module_delete_cdev(lp);
	release_mem_region(lp->mem_start,lp->mem_end-lp->mem_start+1);
	kfree(lp);
	dev_set_drvdata(&pdev->dev,NULL);

	printk(KERN_ALERT "The module %s was successfully removed\n",DRIVER_NAME);
	return 0;
}

/*--------------------*/
/* Driver information */
/*--------------------*/

static const struct of_device_id simple_of_match[] = {
     {.compatible = "wcrvt,axi_gp-1.0"},
     {},
};

MODULE_DEVICE_TABLE(of, simple_of_match);

//Make a structure for registration
static struct platform_driver module_driver = {
	.driver = {
	.name = DRIVER_NAME,
	.owner = THIS_MODULE,
	.of_match_table = simple_of_match,
	},
	.probe = module_probe,
	.remove = module_remove,
};

//Registration of a device
module_platform_driver(module_driver);

//Licence
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("wcrvt");
MODULE_DESCRIPTION(DRIVER_NAME ": AXI-GP Driver");
MODULE_ALIAS(DRIVER_NAME);
```
```devicetree.dts
/dts-v1/;/plugin/;
/ {
	fragment@0 {
		target-path = "/amba/fpga-region0";

		__overlay__ {
			#address-cells = <0x1>;
			#size-cells = <0x1>;

			firmware-name = "AC1.bin"; 

			axi-gp@43c00000 {
				compatible = "wcrvt,axi_gp-1.0";
				reg = <0x43c00000 0x1000>;
				interrupt-parent = <&intc>;
				interrupts = <0x0 0x1d 0x1>;
			};
		};
	} ;
} ;
```
↑割り込みにはGIC61番のエッジトリガを使用。

##割り込みハンドラの実装
プローブ処理時に以下のチェックを行って，request_irq()でハンドラの登録を行う。

```simple_irq.c
//Check devicetree and get information <GIC channel and IRQ type>
    irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    if(!irq_res){
        dev_err(&pdev->dev, "cant get irq resource\n");
        return -ENODEV;     /*errno-base.h: No such device (19)*/
    }

    irq = irq_res->start;
    if(irq < 0){
        dev_err(&pdev->dev, "cant get irq resource\n");
        return -ENODEV;     /*errno-base.h: No such device (19)*/
    }

    //Registrate Interrupt Service Routine (ISR)
    if(request_irq(irq, irq_handler, IRQF_TRIGGER_RISING, DEVICE_NAME, NULL)){
        printk(KERN_ERR"fail to request irq %d\n", irq);
        return -EIO;        /*errno-base.h: I/O error (5)*/
    }
    else{
        printk(KERN_INFO"registered IRQ %d\n", irq);
    }
```

```simple_irq_handler.c

//Called function
static irqreturn_t irq_handler(int irq, void *dev_id)
{
	irq_cnt++;
	return IRQ_HANDLED;
}
```

このプローブ処理において，デバイスツリーにデバイスのアドレス範囲を書かないとplatform_get_resource()で弾かれる。そのため，以下のような分離されたデバイスツリー記述と実装は禁止。

```devicetree.dts
/dts-v1/;/plugin/;
/ {
	fragment@0 {
		target-path = "/amba/fpga-region0";

		__overlay__ {
			#address-cells = <0x1>;
			#size-cells = <0x1>;

			firmware-name = "AC1.bin"; 

			uio@43c00000 {
				compatible = "generic-uio";
				reg = <0x43c00000 0x1000>;
			};

			axi-gp-irq@43c10000 {
				compatible = "wcrvt,axi_gp-1.0";
				interrupt-parent = <&intc>;
				interrupts = <0x0 0x1d 0x1>;
			};
		};
	} ;
} ;
```
一応回避は可能だが，危険な記述になる。

```devicetree.dts
/dts-v1/;/plugin/;
/ {
	fragment@0 {
		target-path = "/amba/fpga-region0";

		__overlay__ {
			#address-cells = <0x1>;
			#size-cells = <0x1>;

			firmware-name = "AC1.bin"; 

			uio@43c00000 {
				compatible = "generic-uio";
				reg = <0x43c00000 0x1000>;
			};

			axi-gp-irq@43c10000 {
				compatible = "wcrvt,axi_gp-1.0";
				interrupt-parent = <&intc>;
				interrupts = <0x0 0x1d 0x1>;
				reg = <0x43c00000 0x1000>;
			};
		};
	} ;
} ;
```
@以下のアドレスを書かないと警告されるため警告回避のためにはダミーを用意したが，これも他人が理解できないため危険。そもそもこのアドレスにデバイスを登録する可能性も十分にある。当然だが，デバイスがPL上に設置した回路一つなら，デバイスの登録はひとつであるべき。そのため，mmapを再実装した。

##mmapの実装
Linuxデバイスドライバ 第3版 15章2節に簡単なmmap実装が書かれている。

```simple_mmap.c
static struct vm_operations_struct simple_remap_vm_ops={
    .open=simple_open,
    .close=simple_close,
};

static int module_mmap(struct file *file,struct vm_area_struct *vma){
    vma->vm_ops=&simple_remap_vm_ops;
    if(remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,size, vma->vm_page_prot)) return -EAGAIN;
    return 0;
}
```
vma->vm_pgoffはユーザ空間でmmapを行った際に指定するアドレスオフセットをPAGE_SHIFT分右シフトしたもの。そのため，platform_get_resource()で取得したアドレスをPAGE_SHIFT分右シフトしてremap_pfn_range()に渡してあげれば，デバイスツリーで指定したデバイスに関してメモリマップドI/Oができる。ただし，もう一処理行わないとI/Oとのマッピングが上手くいかなかった。

Zynq PLにLED点灯回路を設置してPSから点灯を試みたが，上記のmmap実装では点灯しない。

```userspace.c
	int fd;
	if((fd=open("/dev/axi-gp0",O_RDWR))==-1){ printf("Can not open /dev/axi-gp0\n"); exit(1);}
	map_addr=(unsigned int*)mmap(NULL,0x1000,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
```
一方で，/dev/memを開けば点灯した。

```userspace.c
	int fd;
	if((fd=open("/dev/mem0",O_RDWR))==-1){ printf("Can not open /dev/mem\n"); exit(1);}
	map_addr=(unsigned int*)mmap(NULL,0x1000,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0x43c00000);
```
違いがよく分からないため，/dev/memを実装しているカーネルソースのdriver/mem.cを確認したところ，次の一文を見つけた。

```driver/mem.c
	vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
						 size,
						 vma->vm_page_prot);
```
この一文をmmap実装に追加したら，点灯するようになった。

##所感
https://qiita.com/ikwzm/items/ec514e955c16076327ce にて紹介されているdevicetree overlayドライバを使用するとデバイスの登録と解除が自由なので，デバッグ時に非常に楽だった。また，LEDが乗ったzynqボードを使えばI/O操作のデバイスドライバの動作確認が簡単に行えた。zynqはデバイスドライバの勉強にも向いていると思います。