LoginSignup
8
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-15

概要

https://qiita.com/wcrvt/items/e1957534bc774c628d13 の続き
割り込み時に,カーネル空間で浮動小数点数演算ができなかったのでユーザ空間に通知したい。ARMではカーネル空間でNEON命令がサポートされているため(kernel mode NEON),できないことはないがロボット制御用の演算をしたいのでユーザ空間に通知したかった(Bottom Halfをユーザ空間で行いたかった)。

実装方法

fasyncを使用して,kill_fasyncでシグナルを発生させました。別の目的として割り込み応答速度の測定を行いたかったため,割り込み禁止/許可を抜いた簡単なものになっています。ついでに前回のコード(https://qiita.com/wcrvt/items/e1957534bc774c628d13 )に無駄が多かったので整理した。

zynq_pl.c

#include <linux/cdev.h>
#include <linux/idr.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/of_platform.h>
#include <linux/slab.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

static int irq;

int minor;
unsigned long long irq_cnt;

struct class *zynq_class;
static struct cdev *zynq_cdev;
static int zynq_major;

static unsigned long zynq_mem_start;
static unsigned long zynq_mem_size;
static struct fasync_struct *async_queue=NULL;

#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 axi_open(struct inode *inode, struct file *filp){
    return 0;
}

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

static ssize_t axi_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 axi_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;
}

static int axi_fasync(int fd, struct file *filp, int on)
{
    const int ret_val = fasync_helper(fd, filp, on, &async_queue);
    if(ret_val <= 0) printk(KERN_WARNING "failed at fasync_helper()\r\n");

    return 0;
}


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

static int axi_mmap(struct file *filp,struct vm_area_struct *vma){

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

    phys_addr_t offset=(phys_addr_t) zynq_mem_start;
    unsigned long pgoff= zynq_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(filp,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;
}

static const struct file_operations module_fops = {
 .owner   = THIS_MODULE,
  .open     = axi_open,
  .release  = axi_close,
 .read        = axi_read,
 .write   = axi_write,
 .fasync  = axi_fasync,
  .mmap     = axi_mmap,
};

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

static irqreturn_t irq_handler(int irq, void *dev_id)
{
    if (async_queue) kill_fasync(&async_queue, SIGIO, POLL_IN);
    irq_cnt++;
    return IRQ_HANDLED;
}

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

static int module_create_cdev(void){

    struct cdev *cdev=NULL;
    dev_t dev=0;

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

    zynq_major=MAJOR(dev);
    cdev_init(cdev,&module_fops);
    cdev->owner=THIS_MODULE;

    if(cdev_add(cdev,dev,MINOR_NUM)<0){
        printk(KERN_ERR "cdev_add failed\n");
        return -1;
    }

    zynq_class=class_create(THIS_MODULE,"axi_gp");
    if(IS_ERR(zynq_class)){
        printk(KERN_ERR "class create failed");
        cdev_del(cdev);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }

    for(minor=0;minor<MINOR_NUM;minor++){
        device_create(zynq_class,NULL,MKDEV(zynq_major,minor),NULL,"%s%d",DEVICE_BASE,minor);
    }

    zynq_cdev=cdev;

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

}


static void module_delete_cdev(void)
{
        dev_t dev;
    dev = MKDEV(zynq_major,MINOR_BASE);

    for (minor=MINOR_BASE; minor<MINOR_BASE+MINOR_NUM; minor++){
        device_destroy(zynq_class, MKDEV(zynq_major, minor));
    }

    class_destroy(zynq_class);
    cdev_del(zynq_cdev);
    unregister_chrdev_region(dev, MINOR_NUM);
}


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

//prove process
static int module_probe(struct platform_device *pdev)
{

    struct resource *io_resource;
    struct resource *irq_resource;

    irq_cnt=0;

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

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

    zynq_mem_start = io_resource->start;
    zynq_mem_size = io_resource->end - io_resource->start + 1;


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

    irq = irq_resource->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);
    }

  if(module_create_cdev()) return -EIO;

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

    return 0;
}

//Remove process
static int module_remove(struct platform_device *pdev)
{
    free_irq(irq, NULL);
    module_delete_cdev();
    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);

fasync実装部分は

fasync_add.c
static struct fasync_struct *async_queue=NULL;

static int axi_fasync(int fd, struct file *filp, int on)
{
    const int ret_val = fasync_helper(fd, filp, on, &async_queue);
    if(ret_val <= 0) printk(KERN_WARNING "failed at fasync_helper()\r\n");

    return 0;
}

ユーザ空間通知の部分は

notification_irq.c
static irqreturn_t irq_handler(int irq, void *dev_id)
{
    if (async_queue) kill_fasync(&async_queue, SIGIO, POLL_IN);
    irq_cnt++;
    return IRQ_HANDLED;
}

これにより発生するシグナルをユーザ空間で受け取る。ユーザ空間で下記の設定を行い,シグナル受付の準備をする。

userspace.c
    int axi_fd;
    volatile unsigned int* map_addr;    //Shared with several files; registers connecting to the FPGA.
    if((axi_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,axi_fd,0);

    struct sigaction sa_sigio;
    memset(&sa_sigio,0,sizeof(struct sigaction));
    sa_sigio.sa_handler=sigio; //←ユーザ定義, 後で定義
    sa_sigio.sa_flags = SA_RESTART;
    const int oflag_sigio=fcntl(axi_fd,F_GETFL);
    if(sigaction(SIGIO,&sa_sigio,NULL)<0) perror("sigaction: SIGIO");
    if(fcntl(axi_fd,F_SETOWN,getpid())<0) perror("fcntl:F_GETOWN");
    if(fcntl(axi_fd,F_SETFL,oflag_sigio|FASYNC)<0) perror("fcntl:F_SETFL");

このデバイスドライバを作った本来の目的は,割り込み応答をUIOより早くしたかったからです。計測用にZynq PLにAXI Slaveを持つ回路を設置しました。回路の一部を抜粋します。

axi_time_measure

    //AXI Slaveの読取り時の動作を変更する
    wire [C_S_AXI_DATA_WIDTH-1:0]   rd_slv_reg0;
    wire [C_S_AXI_DATA_WIDTH-1:0]   rd_slv_reg1;
    wire [C_S_AXI_DATA_WIDTH-1:0]   rd_slv_reg2;
    wire [C_S_AXI_DATA_WIDTH-1:0]   rd_slv_reg3;

    assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
    always @(*)
    begin
          // Address decoding for reading registers
          case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
            2'h0   : reg_data_out <= rd_slv_reg0;
            2'h1   : reg_data_out <= rd_slv_reg1;
            2'h2   : reg_data_out <= rd_slv_reg2;
            2'h3   : reg_data_out <= rd_slv_reg3;
            default : reg_data_out <= 0;
          endcase
    end
axi_time_measure
    // Add user logic here

    wire clk;
    assign clk=S_AXI_ACLK;

    assign led=slv_reg0[3:0];

    wire ena_irq=slv_reg0[0];
    wire ena_tsc=slv_reg0[1];
    wire ena_irqcnt=slv_reg0[2];

    wire [31:0] irqgen_cnt_max;
    assign irqgen_cnt_max = slv_reg1;

    wire wsig;
    assign wsig=slv_reg2[0];

    reg [31:0] tsc_sec_ps;
    reg [31:0] tsc_nsec_ps;
    reg [31:0] rtrip_get_ps;
    reg [31:0] irq_num_ps;
    assign rd_slv_reg0 = tsc_sec_ps;
    assign rd_slv_reg1 = tsc_nsec_ps;
    assign rd_slv_reg2 = rtrip_get_ps;
    assign rd_slv_reg3 = irq_num_ps;

    //Interruput
    wire irq_pl;
    reg [31:0] irqgen_cnt; initial irqgen_cnt=1'b0;
    localparam [31:0] irq_on_st=32'd10;
    localparam [31:0] irq_on_ed=32'd110;
    assign irq_ps=(irqgen_cnt>irq_on_st & irqgen_cnt<irq_on_ed)? 1'b1:1'b0;
    assign irq_pl=(irqgen_cnt==irqgen_cnt_max)? 1'b1:1'b0;
    always@(posedge clk) begin irqgen_cnt<=(ena_irq==1'b1)? ((irqgen_cnt==irqgen_cnt_max)? 1'b0:irqgen_cnt+32'b1):1'b0; end

    //Timestamp counter
    reg [31:0] tsc_sec=1'b0;
    reg [31:0] tsc_nsec=1'b0;
    localparam [31:0] cnt_1e8 = 32'd99999999;
    always@(posedge clk)
    begin
        tsc_sec <=(ena_tsc==1'b1)? ((tsc_nsec==cnt_1e8)? tsc_sec+32'b1:tsc_sec):1'b0;
        tsc_nsec<=(ena_tsc==1'b1)? ((tsc_nsec==cnt_1e8)? 1'b0:tsc_nsec+32'b1):1'b0;
    end

    //Write signal
    reg [1:0] wsig_buf=1'b0;
    always@(posedge clk) wsig_buf<={wsig_buf[0],wsig};
    wire wflag=^wsig_buf;

    //Round trip time
    reg [31:0] rtrip_cnt=1'b0;
    reg [31:0] rtrip_get=1'b0;
    always@(posedge clk)
    begin
        rtrip_cnt<=(irqgen_cnt==irq_on_st)? 1'b0:rtrip_cnt+1'b1;
        rtrip_get<=(wflag)? rtrip_cnt:rtrip_get;
    end

    //IRQ pulse counter
    reg [31:0] irq_num=1'b0;
    always@(posedge clk)
    begin
        if(ena_irqcnt) irq_num<=(irqgen_cnt==irq_on_st)? irq_num+1'b1:irq_num;
        else irq_num=1'b0;
        irq_num_ps<=irq_num;
    end

    //Buffer
    always@(posedge clk)
    begin
        tsc_sec_ps<=(irq_pl==1'b1)? tsc_sec:tsc_sec_ps;
        tsc_nsec_ps<=(irq_pl==1'b1)? ((tsc_nsec==1'b0)? 1'b0:tsc_nsec+32'b1):tsc_nsec_ps;
        rtrip_get_ps<=(irq_pl==1'b1)? rtrip_get:rtrip_get_ps;
    end


    // User logic ends

PSからの書き込みに関しては,slv_reg0をenable信号,slv_reg1を割り込み発生周期の設定レジスタ,slv_reg2をユーザ空間からの書き込み応答確認レジスタにしています。PSからの読み込みに関しては,rd_slv0を経過時間(秒),rd_slv1を経過時間(1e-8秒), rd_slv2を割り込み発生からrd_slv_reg2書き込みまでの経過時間,rd_slv_reg3を割り込み信号発生数にしています。

この回路をZynqでロードして,ユーザ空間で次のプログラムを回しました。

userspace.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sched.h>
#include <time.h>

void sigio(int sig){
    (void)sig;
    static int sigio_cnt=0;
    sigio_cnt++;
    if((sigio_cnt%10000)==0) printf("\nsigio_cnt=%d\n",sigio_cnt);
}

int main(){

    int axi_fd;
    volatile unsigned int* map_addr;    //Shared with several files; registers connecting to the FPGA.
    if((axi_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,axi_fd,0);

    struct sigaction sa_sigio;
    memset(&sa_sigio,0,sizeof(struct sigaction));
    sa_sigio.sa_handler=sigio;
    sa_sigio.sa_flags = SA_RESTART;
    const int oflag_sigio=fcntl(axi_fd,F_GETFL);
    if(sigaction(SIGIO,&sa_sigio,NULL)<0) perror("sigaction: SIGIO");
    if(fcntl(axi_fd,F_SETOWN,getpid())<0) perror("fcntl:F_GETOWN");
    if(fcntl(axi_fd,F_SETFL,oflag_sigio|FASYNC)<0) perror("fcntl:F_SETFL");

    const double Ts_ps=100e-6;
    const double Ts_pl=1e-8;
    const unsigned int irq_cntmax=(unsigned int) (Ts_ps/Ts_pl-1);

    static unsigned int time_sec;
    static unsigned int time_nsec;
    static unsigned int rtime_nsec;
    static unsigned int irq_num;

    static double gt=0.0;
    static double rt=0.0;
    static unsigned int wsig=0;

    wsig=0;

    map_addr[0]=0x0;
    map_addr[1]=irq_cntmax;
    map_addr[2]=0x0;
    map_addr[3]=0x0;

    map_addr[0]=(unsigned int) 4;
    usleep(1000);

    map_addr[0]=(unsigned int) 7;
    usleep(100000);

    for(int i=0;i<30000;i++){
        wsig=~wsig;
        map_addr[2]=wsig;
        sleep(10); //or pause();
    }

    map_addr[0]=0x06;
    time_sec=map_addr[0];
    time_nsec=map_addr[1];
    gt=time_sec+time_nsec*1e-8;
    rtime_nsec=map_addr[2];
    rt=rtime_nsec*1e-8;
    irq_num=map_addr[3];

    printf("time=%lfus\n",Ts_ps*1e6);
    printf("time=%lfs,irq_num=%d\n",gt,irq_num);
    printf("round trip time=%lfus\n",rt*1e6);

    //End processing
    for(int i=0;i<4;i++) map_addr[i]=0x00;

    munmap((void*)map_addr, 0x1000);

    return 0;
}

このプログラムを回したところ,割り込み発生からPS応答までの時間(rd_slv_reg2値から取得)は30us程度でした。UIOを使用してこの時間を測定したときは50us程度だった(https://qiita.com/wcrvt/items/23f6e0955bbc6dea79d1 )ので,少し早くなりました。

UIOの実装を見る

UIOを用いた簡易割り込みが紹介されています。
https://qiita.com/ikwzm/items/b22592c31cdbb9ab6cf7
この動作を見るために,カーネルソースのdrivers/uioの中にuio.cを見てみます。

上の記事では,実装されたUIOの/dev/uio0に対してreadを行うと割り込み信号が来るまで待機する(プロセスがブロックされる)ので,簡易的なイベント駆動ができることを紹介してくれています。該当コードを見ると,

uio.c
static ssize_t uio_read(struct file *filep, char __user *buf,
            size_t count, loff_t *ppos)
{
    struct uio_listener *listener = filep->private_data;
    struct uio_device *idev = listener->dev;
    DECLARE_WAITQUEUE(wait, current);
    ssize_t retval;
    s32 event_count;

    if (!idev->info->irq)
        return -EIO;

    if (count != sizeof(s32))
        return -EINVAL;

    add_wait_queue(&idev->wait, &wait);

    do {
        set_current_state(TASK_INTERRUPTIBLE);

        event_count = atomic_read(&idev->event);
        if (event_count != listener->event_count) {
            __set_current_state(TASK_RUNNING);
            if (copy_to_user(buf, &event_count, count))
                retval = -EFAULT;
            else {
                listener->event_count = event_count;
                retval = count;
            }
            break;
        }

        if (filep->f_flags & O_NONBLOCK) {
            retval = -EAGAIN;
            break;
        }
        //シグナル飛んできたとき
        if (signal_pending(current)) {
            retval = -ERESTARTSYS;
            break;
        }
        schedule();
    } while (1);

    __set_current_state(TASK_RUNNING);
    remove_wait_queue(&idev->wait, &wait);

    return retval;
}

といった形で待ち行列を使ったプロセスブロックの実装となっています。ただ,Linuxデバイスドライバ 第3版 6章4節に書いてあるようにreadで定期的に確認をするのは重そうなので,非同期通知がしたい。

一旦readは置いておいて,割り込みの実装部分を見てみると,

uio.c
void uio_event_notify(struct uio_info *info)
{
    struct uio_device *idev = info->uio_dev;

    atomic_inc(&idev->event);
    //割り込み許可ありの待ちを起こす->readでset_current_state(TASK_INTERRUPTIBLE)してるもの
    wake_up_interruptible(&idev->wait);
    kill_fasync(&idev->async_queue, SIGIO, POLL_IN);
}
EXPORT_SYMBOL_GPL(uio_event_notify);

/**
 * uio_interrupt - hardware interrupt handler
 * @irq: IRQ number, can be UIO_IRQ_CYCLIC for cyclic timer
 * @dev_id: Pointer to the devices uio_device structure
 */
static irqreturn_t uio_interrupt(int irq, void *dev_id)
{
    struct uio_device *idev = (struct uio_device *)dev_id;
    irqreturn_t ret = idev->info->handler(irq, idev->info);

    if (ret == IRQ_HANDLED)
        uio_event_notify(idev->info);

    return ret;
}

割り込みがきたら,uio_device構造体のwait_queue_head_t waitを起こして,シグナルを送信してくれている。割り込みハンドラからユーザ空間に通知したら割り込み応答が30usくらいで,このハンドラからread内の処理を介してユーザ空間に通知すると割り込み応答が50usくらいになるで,単純計算でwaitqueueを使って迂回する時間に20us程度かかっているみたいです。

UIOは一度割り込みを受け付けた後に,自動的に割り込みを禁止するそうです(命令記述見つけられませんでした)。この割り込み許可は,/dev/uio0のwriteをコールする必要があります。したがって,連続的にkill_fasyncでシグナルを送ってもらうことはできず,毎回writeをコールしてirqcontrol(idev->info, 1)をする必要があります。readを使用した簡易割り込みも,readコール後に応答が返ってきた後にwriteをコールし,readをコールして待機するといった流れになります。

uio.c
static ssize_t uio_write(struct file *filep, const char __user *buf,
            size_t count, loff_t *ppos)
{
    struct uio_listener *listener = filep->private_data;
    struct uio_device *idev = listener->dev;
    ssize_t retval;
    s32 irq_on;

    if (!idev->info->irq)
        return -EIO;

    if (count != sizeof(s32))
        return -EINVAL;

    if (!idev->info->irqcontrol)
        return -ENOSYS;

    if (copy_from_user(&irq_on, buf, count))
        return -EFAULT;

    //これ
    retval = idev->info->irqcontrol(idev->info, irq_on);

    return retval ? retval : sizeof(s32);
}

とりあえず,UIOのreadの返り待ちより非同期通知の方が応答が早そうということがわかりました。しかし根本的にユーザ空間で割り込みのBottom Half処理を行うのが遅いようなので,次はFreeRTOSを使いたいと思います。

所感

UIOの割り込みはwaitqueueを使用した綺麗な実装なので,ユーザ空間でBottom Halfを実行したい場合には自分でドライバを作ったりする意味はあまりない気がしました。

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2