精华内容
下载资源
问答
  • Freesclae i.MX6 Linux PCIE驱动源码分析

    千次阅读 2018-10-30 17:14:13
    Freesclae i.MX6 Linux PCIE驱动源码分析   转自: http://www.lai18.com/content/2232856.html     最近需要做一个工具来测试PCIE的link是否成功,但是由于PCIE的驱动都是在内核空间中,因此需要首先分析...

    Freesclae i.MX6 Linux PCIE驱动源码分析

     

    转自:

    http://www.lai18.com/content/2232856.html

     

     

    最近需要做一个工具来测试PCIE的link是否成功,但是由于PCIE的驱动都是在内核空间中,因此需要首先分析一下i.MX6 PCIE的驱动源码。首先我不得不吐槽一下驱动源码的存放位置很混乱,在Linux 3.0.35_4.1.0中,驱动居然是在arch/arm/mach-mx6/目录下,通常的情况来说,这里是存放板极信息文件的地方,而pcie的驱动更应该放在drivers/pci等相关目录下,因此menuconfig的时候也是在一个很奇怪的地方配置。索性到了Linux 3.10.17版本,pcie的代码挪到drivers/pci/pcie/目录下了,不然看起来真的是太纠结了。
            这里再说些废话,不想下载LTIB的朋友就自己去clone下面的git源:

    git://git.freescale.com/imx/linux-2.6-imx.git http://git.freescale.com/git/cgit.cgi/imx/linux-2.6-imx.git 
    不过貌似http的源很慢,git源的速度还行。这里再多说一点,3.0.35之前的内核都是freescale内部维护,并且有一个public git网站,http://git.freescale.com/git/ 上面有大部分freescale自己维护的项目,因此也是走相对封闭的路线。到了3.10.17以后freescale开始走社区的路线,基本上所有的代码都与开源社区同步,连编译环境也从LTIB变成了yocto,基本就是走社区开放的路线。3.10.17以后arch/arm/mach-mx6/目录被删除,取而代之的是设备树,pcie驱动也到了它应该待的地方。
    由于3.0.35以及3.10.17都有不少人在用,我决定两个版本都去分析一下,下面先来分析一下3.0.35_4.1.0内核的版本:
    git checkout imx_3.0.35_4.1.0
    make ARCH=arm imx6_defconfig
    make ARCH=arm menuconfig
    在内核中使能pcie驱动,具体位置(真是一个非常少见的位置)在:

    │     -> System Type                                                                   │  
      │       -> Freescale MXC Implementations 

        │          -> PCI Express support

    后面的EP mode和RC mode都不需要选(它们对应IMX_PCIE_EP_MODE_IN_EP_RC_SYS以及IMX_PCIE_RC_MODE_IN_EP_RC_SYS的宏定义),默认情况下就是RC (Root Complex) mode,这里只能选择built-in 或者no,没有开启编译模块的选项,要开的话自己改一下Kconfig就可以了。代码位于arch/arm/mach-mx6/pcie.c,下面到了我们分析源码的时候了:
    首先看到最后几行:
     

    static int __init imx_pcie_drv_init(void){	return platform_driver_register(&imx_pcie_pltfm_driver);}
    static void __exit imx_pcie_drv_exit(void){	platform_driver_unregister(&imx_pcie_pltfm_driver);}
    module_init(imx_pcie_drv_init);module_exit(imx_pcie_drv_exit);



    在pcie驱动外部加了一层platform driver,下面看imx_pcie_pltfm_driver结构体定义:
     

    static struct platform_driver imx_pcie_pltfm_driver = {	.driver = {		.name	= "imx-pcie",		.owner	= THIS_MODULE,	},	.probe		= imx_pcie_pltfm_probe,};



    可以看到这里通过.driver.name字段来匹配platform driver,匹配之后调用imx_pcie_pltfm_probe函数进行探测,下面就需要去板极文件找添加"imx-pcie"字段platform device的代码了。
    对于i.MX6Q SABRESD平台,板极文件在arch/arm/mach-mx6/board-mx6q_sabresd.c中,而添加pcie设备的代码可以找到在:
     

    /* Add PCIe RC interface support */	imx6q_add_pcie(&mx6_sabresd_pcie_data);



    而imx6q_add_pcie()这个宏是定义在arch/arm/mach-mx6/devices-imx6q.h中的如下几行:
     

    extern const struct imx_pcie_data imx6q_pcie_data __initconst;#define imx6q_add_pcie(pdata) imx_add_pcie(&imx6q_pcie_data, pdata)



    而imx_add_pcie是位于arch/arm/plat-mxc/devices/platform-imx-pcie.c中的,相关代码如下:
     

    struct platform_device *__init imx_add_pcie(		const struct imx_pcie_data *data,		const struct imx_pcie_platform_data *pdata){	struct resource res[] = {		{			.start = data->iobase,			.end = data->iobase + data->iosize - 1,			.flags = IORESOURCE_MEM,		}, {			.start = data->irq,			.end = data->irq,			.flags = IORESOURCE_IRQ,		},	};
    	if (!fuse_dev_is_available(MXC_DEV_PCIE))		return ERR_PTR(-ENODEV);
    	return imx_add_platform_device("imx-pcie", -1,			res, ARRAY_SIZE(res),			pdata, sizeof(*pdata));}



    看到了才找到了真正需要的代码,resource是用来定义寄存器地址以及中断注册信息的,这里最后几行中我用红色加粗斜体字标出了imx-pcie字段,因为这个函数imx_add_platform_device正式我们要找的添加platform device函数,并且通过函数名匹配,这里可以看到pci的platform设备和platform驱动的名称都是imx-pcie,因此驱动和设备通过platform bus得以匹配。同时还通过struct imx_pcie_platform_data *pdata来传递驱动的platform_data,pdata的结构体声明在arch/arm/plat-mxc/include/mach/pcie.h中:
     

    /** * struct imx_pcie_platform_data - optional platform data for pcie on i.MX * * @pcie_pwr_en:	used for enable/disable pcie power (-EINVAL if unused) * @pcie_rst:		used for reset pcie ep (-EINVAL if unused) * @pcie_wake_up:	used for wake up (-EINVAL if unused) * @pcie_dis:		used for disable pcie ep (-EINVAL if unused) */
    struct imx_pcie_platform_data {	unsigned int pcie_pwr_en;	unsigned int pcie_rst;	unsigned int pcie_wake_up;	unsigned int pcie_dis;	unsigned int type_ep; /* 1 EP, 0 RC */};#endif /* __ASM_ARCH_IMX_PCIE_H */



    待会在驱动可以看到这个platform_data会在probe函数中被获取,这种在platform device中添加platform_data来向驱动传递额外的信息在内核中常常能见到。到这里,在内核中添加板级信息的部分已经结束了,内核在启动过程中会去执行init_machine()函数,而init_machine是一个函数指针,它指向的就是arch/arm/mach-mx6/board-mx6q_sabresd.c中的mx6_sabresd_board_init函数,可以看到这个结构体初始化语句:
     

    /* * initialize __mach_desc_MX6Q_SABRESD data structure. */MACHINE_START(MX6Q_SABRESD, "Freescale i.MX 6Quad/DualLite/Solo Sabre-SD Board")	/* Maintainer: Freescale Semiconductor, Inc. */	.boot_params = MX6_PHYS_OFFSET + 0x100,	.fixup = fixup_mxc_board,	.map_io = mx6_map_io,	.init_irq = mx6_init_irq,	.init_machine = mx6_sabresd_board_init,	.timer = &mx6_sabresd_timer,	.reserve = mx6q_sabresd_reserve,MACHINE_END



    到这里应该就能理解添加板级信息的整个过程了,然后就是进行驱动加载了,可以看到驱动加载是在pcie.c中的module_init()中进行的。一切就绪以后就开始跳向probe指针指向的imx_pcie_pltfm_probe,说实话个人觉得这个probe算是比较短的了。大体来分析一下:
     

    mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);if (!mem) {	dev_err(dev, "no mmio space\n");	return -EINVAL;}



    这里的platform_get_resource()就是获取之前struct resource res[]={...}定义的内容,也就是与寄存器base address以及irq相关的信息。
     

    /*  Added for PCI abort handling */	hook_fault_code(16 + 6, imx6q_pcie_abort_handler, SIGBUS, 0,			"imprecise external abort");



    这里像是注册一个abort handling function,具体什么作用也不清楚。
     

    base = ioremap_nocache(PCIE_ARB_END_ADDR - SZ_1M + 1, SZ_1M - SZ_16K);if (!base) {	pr_err("error with ioremap in function %s\n", __func__);	ret = PTR_ERR(base);	return ret;}



    ioremap,为这个区间段的寄存器建立页表映射到内核空间,进行访问。
    这里PCIE_ARB_END_ADDR的定义在arch/arm/plat-mxc/include/mach/mx6.h中:
     

    #define PCIE_ARB_BASE_ADDR		0x01000000#define PCIE_ARB_END_ADDR		0x01FFFFFF



    显然base访问的是[PCIE_ARB_END_ADDR - SZ_1M + 1, PCIE_ARB_END_ADDR - SZ_16K]这一段
     

    dbi_base = devm_ioremap(dev, mem->start, resource_size(mem));if (!dbi_base) {	dev_err(dev, "can't map %pR\n", mem);	ret = PTR_ERR(dbi_base);	goto err_base;}



    这里用的devm_ioremap与ioremap很相似,唯一的区别引用一下这个网页上的回复http://www.spinics.net/lists/devicetree/msg07744.html的:In
    the PCIv3 driver, use devm_ioremap() instead of just ioremap(). when remapping the system controller in the PCIv3 driver, so the mapping will be automatically released on probe failure.
    而这里dbi_base经过对platform-imx-pcie.c中的分析,访问的是[ PCIE_ARB_END_ADDR - SZ_16K, PCIE_ARB_END_ADDR]这一段
     

    /* FIXME the field name should be aligned to RM */imx_pcie_clrset(IOMUXC_GPR12_APP_LTSSM_ENABLE, 0 << 10, IOMUXC_GPR12);
    /* configure constant input signal to the pcie ctrl and phy */if (pdata->type_ep & 1)	/* EP */	imx_pcie_clrset(IOMUXC_GPR12_DEVICE_TYPE,			PCI_EXP_TYPE_ENDPOINT	<< 12, IOMUXC_GPR12);else	/* RC */	imx_pcie_clrset(IOMUXC_GPR12_DEVICE_TYPE,			PCI_EXP_TYPE_ROOT_PORT << 12, IOMUXC_GPR12);imx_pcie_clrset(IOMUXC_GPR12_LOS_LEVEL, 9 << 4, IOMUXC_GPR12);
    imx_pcie_clrset(IOMUXC_GPR8_TX_DEEMPH_GEN1, 0 << 0, IOMUXC_GPR8);imx_pcie_clrset(IOMUXC_GPR8_TX_DEEMPH_GEN2_3P5DB, 0 << 6, IOMUXC_GPR8);imx_pcie_clrset(IOMUXC_GPR8_TX_DEEMPH_GEN2_6DB, 20 << 12, IOMUXC_GPR8);imx_pcie_clrset(IOMUXC_GPR8_TX_SWING_FULL, 127 << 18, IOMUXC_GPR8);imx_pcie_clrset(IOMUXC_GPR8_TX_SWING_LOW, 127 << 25, IOMUXC_GPR8);



    寄存器配置,其中对于PCIE的link成功与否比较关键的就是IOMUXC_GPR8和IOMUXC_GPR12,不过在reference manual中IOMUXC_GPR12的值已经被定死了,所以只能调节IOMUXC_GPR8了。
    这里imx_pcie_clrset是一个内联函数,定义如下:

    /* IMX PCIE GPR configure routines */
    static inline void imx_pcie_clrset(u32 mask, u32 val, void __iomem *addr)
    {
    writel(((readl(addr) & ~mask) | (val & mask)), addr);
    }

    鉴于这里已经可以直接对地址进行直接读写,那么这块内存区域就已经被映射过了。找到IOMUXC_GPR8的定义,在arch/arm/mach-mx6/crm_regs.h文件中:

    /* IOMUXC */
    #define MXC_IOMUXC_BASE
    MX6_IO_ADDRESS(MX6Q_IOMUXC_BASE_ADDR)
    …………
    #define IOMUXC_GPR8
    (MXC_IOMUXC_BASE + 0x20)

    这里MX6Q_IOMUXC_BASE_ADDR被定义在arch/arm/plat-mxc/include/mach/mx6.h中:
    #define AIPS1_ARB_BASE_ADDR
    0x02000000
    ………………
    #define ATZ1_BASE_ADDR
    AIPS1_ARB_BASE_ADDR
    …………
    #define AIPS1_OFF_BASE_ADDR
    (ATZ1_BASE_ADDR + 0x80000)
    …………
    #define MX6Q_IOMUXC_BASE_ADDR
    (AIPS1_OFF_BASE_ADDR + 0x60000)
    最终的值也就是0x0200_0000+0x8_0000+0x6_0000 = 0x020E_0000,参考i.MX6Q reference manual可以找到IOMUXC寄存器范围:[020E_0000, 020E_3FFF] 这里和驱动相对应。
    而MX6_IO_ADDRESS的定义在arch/arm/plat-mxc/include/mach/mx6.h中,定义如下的:
    #define PERIPBASE_VIRT
    0xF2000000
    …………
    #define MX6_IO_ADDRESS(x) (void __force __iomem *)((x) + PERIPBASE_VIRT)
    基本上就是对基地址的一个偏移量。
    注意:直接这样访问是无效的,首先必须要在MMU中建立页表映射到这段地址,即ioremap以后才可以从内核空间访问(这里能访问是因为这块地址之前肯定已经映射过了),只不过映射之后的关系是物理地址增加一个偏移量而已。
     

    /* Enable the pwr, clks and so on */imx_pcie_enable_controller(dev);



    如注释所言,使能pcie的电源和时钟等,由于i.mx是由fuse100来供电的,因此需要通过GPIO的某个引脚来控制供电开关。这个函数很关键,下面简单介绍一下, 本身也不长。
    /* Enable PCIE power */
     

    gpio_request(pdata->pcie_pwr_en, "PCIE POWER_EN");
    /* activate PCIE_PWR_EN */gpio_direction_output(pdata->pcie_pwr_en, 1);
    imx_pcie_clrset(IOMUXC_GPR1_TEST_POWERDOWN, 0 << 18, IOMUXC_GPR1);



    获取gpio引脚的使用权,并且将该gpio输出高电平。最后再写一个测试寄存器。
     

    /* enable the clks */	if (pdata->type_ep) {		pcie_clk = clk_get(NULL, "pcie_ep_clk");		if (IS_ERR(pcie_clk))			pr_err("no pcie_ep clock.\n");
    		if (clk_enable(pcie_clk)) {			pr_err("can't enable pcie_ep clock.\n");			clk_put(pcie_clk);		}	} else {		pcie_clk = clk_get(NULL, "pcie_clk");		if (IS_ERR(pcie_clk))			pr_err("no pcie clock.\n");
    		if (clk_enable(pcie_clk)) {			pr_err("can't enable pcie clock.\n");			clk_put(pcie_clk);		}	}



    这里是获取时钟的函数,有兴趣的可以自己google一下linux的时钟框架
     

    imx_pcie_clrset(IOMUXC_GPR1_PCIE_REF_CLK_EN, 1 << 16, IOMUXC_GPR1);



    这条我猜应该是使能pcie控制器了。
     

    /* start link up */	imx_pcie_clrset(IOMUXC_GPR12_APP_LTSSM_ENABLE, 1 << 10, IOMUXC_GPR12);



    开始link up,我的理解应该是类似于以太网中的ping操作。
     

    /* add the pcie port */	add_pcie_port(base, dbi_base, pdata);



    这个函数检验link是否成功并进行相应的操作。
     

    if (imx_pcie_link_up(dbi_base)) {	struct imx_pcie_port *pp = &imx_pcie_port[num_pcie_ports++];
    	pr_info("IMX PCIe port: link up.\n");
    	pp->index = 0;	pp->root_bus_nr = -1;	pp->base = base;	pp->dbi_base = dbi_base;	spin_lock_init(&pp->conf_lock);	memset(pp->res, 0, sizeof(pp->res));}



    先看看如果link up成功的话,那么就进行相关的初始化操作,向内核添加信息。
     

    else {	pr_info("IMX PCIe port: link down!\n");
    	/* Release the clocks, and disable the power */	pcie_clk = clk_get(NULL, "pcie_clk");	if (IS_ERR(pcie_clk))		pr_err("no pcie clock.\n");
    	clk_disable(pcie_clk);	clk_put(pcie_clk);
    	imx_pcie_clrset(IOMUXC_GPR1_PCIE_REF_CLK_EN, 0 << 16,			IOMUXC_GPR1);
    	/* Disable PCIE power */	gpio_request(pdata->pcie_pwr_en, "PCIE POWER_EN");
    	/* activate PCIE_PWR_EN */	gpio_direction_output(pdata->pcie_pwr_en, 0);
    	imx_pcie_clrset(IOMUXC_GPR1_TEST_POWERDOWN, 1 << 18,			IOMUXC_GPR1);}



    如果不成功的话,那么就disable pcie_clk并且释放对pcie_clk的引用,再把pcie控制器disable掉,最后再把PCIE的外部供电通过gpio来disable。

    回到probe函数得最后一部分代码:
     

    pci_common_init(&imx_pci);



    这里是内核中pci驱动初始化函数,具体的位置在:arch/arm/kernel/bios32.c,这里不再分析,基本就是做一些添加pci bus之类的初始化工作,对于arm体系的mpu来说是一样的。

    展开全文
  • linux下PCI驱动源码实例1,该源码缺少pci_fops的初始化 #include <linux/fs.h> #include <linux/module.h> #include <linux/init.h> #include <linux/pci.h> #include <linux/interrupt...
    • linux下PCI驱动源码实例1,该源码缺少pci_fops的初始化
    #include <linux/fs.h>
    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/pci.h>
    #include <linux/interrupt.h>
    #include <asm-generic/signal.h>
    #undef debug
    
    
    // ATTENTION copied from /uboot_for_mpc/arch/powerpc/include/asm/signal.h
    // Maybe it don't work with that
    //____________________________________________________________
    #define SA_INTERRUPT    0x20000000 /* dummy -- ignored */
    #define SA_SHIRQ        0x04000000
    //____________________________________________________________
    
    #define pci_module_init pci_register_driver // function is obsoleted
    
    // Hardware specific part
    #define MY_VENDOR_ID 0x5333
    #define MY_DEVICE_ID 0x8e40
    #define MAJOR_NR     240
    #define DRIVER_NAME  "PCI-Driver"
    
    static unsigned long ioport=0L, iolen=0L, memstart=0L, memlen=0L,flag0,flag1,flag2,temp=0L;
    
    // private_data
    struct _instance_data {
    
        int counter; // just as a example (5-27)
    
        // other instance specific data
    };
    
    // Interrupt Service Routine
    static irqreturn_t pci_isr( int irq, void *dev_id, struct pt_regs *regs )
    {
        return IRQ_HANDLED;
    }
    
    
    // Check if this driver is for the new device
    static int device_init(struct pci_dev *dev,
            const struct pci_device_id *id)
    {
        int err=0;  // temp variable
    
        #ifdef debug
    
        flag0=pci_resource_flags(dev, 0 );
        flag1=pci_resource_flags(dev, 1 );
        flag2=pci_resource_flags(dev, 2 );
        printk("DEBUG: FLAGS0 = %u\n",flag0);
        printk("DEBUG: FLAGS1 = %u\n",flag1);
        printk("DEBUG: FLAGS2 = %u\n",flag2);
    
        /*
         * The following sequence checks if the resource is in the
         * IO / Storage / Interrupt / DMA address space
         * and prints the result in the dmesg log
         */
        if(pci_resource_flags(dev,0) & IORESOURCE_IO)
        {
            // Ressource is in the IO address space
            printk("DEBUG: IORESOURCE_IO\n");
        }
        else if (pci_resource_flags(dev,0) & IORESOURCE_MEM)
        {
            // Resource is in the Storage address space
            printk("DEBUG: IORESOURCE_MEM\n");
        }
        else if (pci_resource_flags(dev,0) & IORESOURCE_IRQ)
        {
            // Resource is in the IRQ address space
            printk("DEBUG: IORESOURCE_IRQ\n");
        }
        else if (pci_resource_flags(dev,0) & IORESOURCE_DMA)
        {
            // Resource is in the DMA address space
            printk("DEBUG: IORESOURCE_DMA\n");
        }
        else
        {
            printk("DEBUG: NOTHING\n");
        }
    
        #endif /* debug */
    
        // allocate memory_region
        memstart = pci_resource_start( dev, 0 );
        memlen = pci_resource_len( dev, 0 );
        if( request_mem_region( memstart, memlen, dev->dev.kobj.name )==NULL ) {
            printk(KERN_ERR "Memory address conflict for device \"%s\"\n",
                    dev->dev.kobj.name);
            return -EIO;
        }
        // allocate a interrupt
        if(request_irq(dev->irq,pci_isr,SA_INTERRUPT|SA_SHIRQ,
                "pci_drv",dev)) {
            printk( KERN_ERR "pci_drv: IRQ %d not free.\n", dev->irq );
        }
        else
        {
            err=pci_enable_device( dev );
            if(err==0)      // enable device successful
            {
                return 0;
            }
            else        // enable device not successful
            {
                return err;
            }
    
        }
        // cleanup_mem
        release_mem_region( memstart, memlen );
        return -EIO;
    }
    // Function for deinitialization of the device
    static void device_deinit( struct pci_dev *pdev )
    {
        free_irq( pdev->irq, pdev );
        if( memstart )
            release_mem_region( memstart, memlen );
    }
    
    static struct file_operations pci_fops;
    
    static struct pci_device_id pci_drv_tbl[] __devinitdata = {
        {       MY_VENDOR_ID,           // manufacturer identifier
            MY_DEVICE_ID,           // device identifier
            PCI_ANY_ID,             // subsystem manufacturer identifier
            PCI_ANY_ID,             // subsystem device identifier
            0,                      // device class
            0,                      // mask for device class
            0 },                    // driver specific data
            { 0, }
    };
    
    static int driver_open( struct inode *geraetedatei, struct file *instance )
    {
        struct _instance_data *iptr;
    
        iptr = (struct _instance_data *)kmalloc(sizeof(struct _instance_data),
                GFP_KERNEL);
        if( iptr==0 ) {
            printk("not enough kernel mem\n");
            return -ENOMEM;
        }
        /* replace the following line with your instructions  */
        iptr->counter= strlen("Hello World\n")+1;    // just as a example (5-27)
    
        instance->private_data = (void *)iptr;
        return 0;
    }
    
    static void driver_close( struct file *instance )
    {
        if( instance->private_data )
            kfree( instance->private_data );
    }
    
    
    static struct pci_driver pci_drv = {
        .name= "pci_drv",
                .id_table= pci_drv_tbl,
                .probe= device_init,
                .remove= device_deinit,
    };
    
    static int __init pci_drv_init(void)
    {    // register the driver by the OS
        if(register_chrdev(MAJOR_NR, DRIVER_NAME, &pci_fops)==0) {
            if(pci_module_init(&pci_drv) == 0 ) // register by the subsystem
                return 0;
            unregister_chrdev(MAJOR_NR,DRIVER_NAME); // unregister if no subsystem support
        }
        return -EIO;
    }
    
    static void __exit pci_drv_exit(void)
    {
        pci_unregister_driver( &pci_drv );
        unregister_chrdev(MAJOR_NR,DRIVER_NAME);
    }
    
    module_init(pci_drv_init);
    module_exit(pci_drv_exit);
    
    MODULE_LICENSE("GPL");

     

      

    •  PCIE同PCI驱动的差异

    From a software standpoint, PCI and PCI Express devices are essentially the same. PCIe devices had the same configuration space, BARs, and (usually) support the same PCI INTx interrupts.一般情况下,两者基本保持一致

    Example #1: Windows XP has no special knowledge of PCIe, but runs fine on PCIe systems.

    Example #2: My company offers both PCI and PCIe versions of a peripheral board, and they use the same Windows/Linux driver package. The driver does not "know" the difference between the two boards.

    However: PCIe devices frequently take advantage of "advanced" features, like MSI, Hotplugging, extended configuration space, etc. Many of these feature existed on legacy PCI, but were unused. If this is a device you are designing, it is up to you whether or not you implement these advanced features.但是pcie在一些高级特性上有优势,比如MSI(Message Signaled Interrupts)、Hotplugging(热插拔)、配置空间扩展等。

    • linux设备驱动程序框架

    Linux将所有外部设备看成是一类特殊文件,称之为“设备文件”,如果说系统调用是Linux内核和应用程序之间的接口,那么设备驱动程序则可以看成是Linux内核与外部设备之间的接口。设备驱动程序向应用程序屏蔽了硬件在实现上的细节,使得应用程序可以像操作普通文件一样来操作外部设备。

    1. 字符设备和块设备

    Linux抽象了对硬件的处理,所有的硬件设备都可以像普通文件一样来看待:它们可以使用和操作文件相同的、标准的系统调用接口来完成打开、关闭、读写和I/O控制操作,而驱动程序的主要任务也就是要实现这些系统调用函数。Linux系统中的所有硬件设备都使用一个特殊的设备文件来表示,例如,系统中的第一个IDE硬盘使用/dev/hda表示。每个设备文件对应有两个设备号:一个是主设备号,标识该设备的种类,也标识了该设备所使用的驱动程序;另一个是次设备号,标识使用同一设备驱动程序的不同硬件设备。设备文件的主设备号必须与设备驱动程序在登录该设备时申请的主设备号一致,否则用户进程将无法访问到设备驱动程序。

    在Linux操作系统下有两类主要的设备文件:一类是字符设备,另一类则是块设备。字符设备是以字节为单位逐个进行I/O操作的设备,在对字符设备发出读写请求时,实际的硬件I/O紧接着就发生了,一般来说字符设备中的缓存是可有可无的,而且也不支持随机访问。块设备则是利用一块系统内存作为缓冲区,当用户进程对设备进行读写请求时,驱动程序先查看缓冲区中的内容,如果缓冲区中的数据能满足用户的要求就返回相应的数据,否则就调用相应的请求函数来进行实际的I/O操作。块设备主要是针对磁盘等慢速设备设计的,其目的是避免耗费过多的CPU时间来等待操作的完成。一般说来,PCI卡通常都属于字符设备。

    所有已经注册(即已经加载了驱动程序)的硬件设备的主设备号可以从/proc/devices文件中得到。使用mknod命令可以创建指定类型的设备文件,同时为其分配相应的主设备号和次设备号。例如,下面的命令:

    [root@gary root]# mknod  /dev/lp0  c  6  0

     

    将建立一个主设备号为6,次设备号为0的字符设备文件/dev/lp0。当应用程序对某个设备文件进行系统调用时,Linux内核会根据该设备文件的设备类型和主设备号调用相应的驱动程序,并从用户态进入到核心态,再由驱动程序判断该设备的次设备号,最终完成对相应硬件的操作。

    2. 设备驱动程序接口

    Linux中的I/O子系统向内核中的其他部分提供了一个统一的标准设备接口,这是通过include/linux/fs.h中的数据结构file_operations来完成的:

    struct file_operations {
            struct module *owner;
            loff_t (*llseek) (struct file *, loff_t, int);
            ssize_t (*read) (struct file *, char *, size_t, loff_t *);
            ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
            int (*readdir) (struct file *, void *, filldir_t);
            unsigned int (*poll) (struct file *, struct poll_table_struct *);
            int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
            int (*mmap) (struct file *, struct vm_area_struct *);
            int (*open) (struct inode *, struct file *);
            int (*flush) (struct file *);
            int (*release) (struct inode *, struct file *);
            int (*fsync) (struct file *, struct dentry *, int datasync);
            int (*fasync) (int, struct file *, int);
            int (*lock) (struct file *, int, struct file_lock *);
            ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
            ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
            ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
            unsigned long (*get_unmapped_area)(struct file *, unsigned long, 
             unsigned long, unsigned long, unsigned long);
    };

     

    当应用程序对设备文件进行诸如open、close、read、write等操作时,Linux内核将通过file_operations结构访问驱动程序提供的函数。例如,当应用程序对设备文件执行读操作时,内核将调用file_operations结构中的read函数。

    3. 设备驱动程序模块

    Linux下的设备驱动程序可以按照两种方式进行编译,一种是直接静态编译成内核的一部分,另一种则是编译成可以动态加载的模块。如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态地卸载,不利于调试,所有推荐使用模块方式。

    从本质上来讲,模块也是内核的一部分,它不同于普通的应用程序,不能调用位于用户态下的C或者C++库函数,而只能调用Linux内核提供的函数,在/proc/ksyms中可以查看到内核提供的所有函数。

    在以模块方式编写驱动程序时,要实现两个必不可少的函数init_module( )和cleanup_module( ),而且至少要包含<linux/krernel.h>和<linux/module.h>两个头文件。在用gcc编译内核模块时,需要加上-DMODULE -D__KERNEL__ -DLINUX这几个参数,编译生成的模块(一般为.o文件)可以使用命令insmod载入Linux内核,从而成为内核的一个组成部分,此时内核会调用模块中的函数init_module( )。当不需要该模块时,可以使用rmmod命令进行卸载,此进内核会调用模块中的函数cleanup_module( )。任何时候都可以使用命令来lsmod查看目前已经加载的模块以及正在使用该模块的用户数。

    4. 设备驱动程序结构

    了解设备驱动程序的基本结构(或者称为框架),对开发人员而言是非常重要的,Linux的设备驱动程序大致可以分为如下几个部分:驱动程序的注册与注销、设备的打开与释放、设备的读写操作、设备的控制操作、设备的中断和轮询处理。

    • 驱动程序的注册与注销

      向系统增加一个驱动程序意味着要赋予它一个主设备号,这可以通过在驱动程序的初始化过程中调用register_chrdev( )或者register_blkdev( )来完成。而在关闭字符设备或者块设备时,则需要通过调用unregister_chrdev( )或unregister_blkdev( )从内核中注销设备,同时释放占用的主设备号。

    • 设备的打开与释放

      打开设备是通过调用file_operations结构中的函数open( )来完成的,它是驱动程序用来为今后的操作完成初始化准备工作的。在大部分驱动程序中,open( )通常需要完成下列工作:

      1. 检查设备相关错误,如设备尚未准备好等。
      2. 如果是第一次打开,则初始化硬件设备。
      3. 识别次设备号,如果有必要则更新读写操作的当前位置指针f_ops。
      4. 分配和填写要放在file->private_data里的数据结构。
      5. 使用计数增1。

      释放设备是通过调用file_operations结构中的函数release( )来完成的,这个设备方法有时也被称为close( ),它的作用正好与open( )相反,通常要完成下列工作:

      1. 使用计数减1。
      2. 释放在file->private_data中分配的内存。
      3. 如果使用计算为0,则关闭设备。
    • 设备的读写操作

      字符设备的读写操作相对比较简单,直接使用函数read( )和write( )就可以了。但如果是块设备的话,则需要调用函数block_read( )和block_write( )来进行数据读写,这两个函数将向设备请求表中增加读写请求,以便Linux内核可以对请求顺序进行优化。由于是对内存缓冲区而不是直接对设备进行操作的,因此能很大程度上加快读写速度。如果内存缓冲区中没有所要读入的数据,或者需要执行写操作将数据写入设备,那么就要执行真正的数据传输,这是通过调用数据结构blk_dev_struct中的函数request_fn( )来完成的。

    • 设备的控制操作

      除了读写操作外,应用程序有时还需要对设备进行控制,这可以通过设备驱动程序中的函数ioctl( )来完成。ioctl( )的用法与具体设备密切关联,因此需要根据设备的实际情况进行具体分析。

    • 设备的中断和轮询处理 

      对于不支持中断的硬件设备,读写时需要轮流查询设备状态,以便决定是否继续进行数据传输。如果设备支持中断,则可以按中断方式进行操作。

    三、PCI驱动程序实现

    1. 关键数据结构

    PCI设备上有三种地址空间:PCI的I/O空间、PCI的存储空间和PCI的配置空间。CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的PCI初始化代码使用。内核在启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号以及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。

    Linux驱动程序通常使用结构(struct)来表示一种设备,而结构体中的变量则代表某一具体设备,该变量存放了与该设备相关的所有信息。好的驱动程序都应该能驱动多个同种设备,每个设备之间用次设备号进行区分,如果采用结构数据来代表所有能由该驱动程序驱动的设备,那么就可以简单地使用数组下标来表示次设备号。

    在PCI驱动程序中,下面几个关键数据结构起着非常核心的作用:

    • pci_driver

      这个数据结构在文件include/linux/pci.h里,这是Linux内核版本2.4之后为新型的PCI设备驱动程序所添加的,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe( )和卸载设备的函数remove( ):

      struct pci_driver {
          struct list_head node;
          char *name;
          const struct pci_device_id *id_table;
          int  (*probe)  (struct pci_dev *dev, const struct pci_device_id *id);
          void (*remove) (struct pci_dev *dev);
          int  (*save_state) (struct pci_dev *dev, u32 state);
          int  (*suspend)(struct pci_dev *dev, u32 state);
          int  (*resume) (struct pci_dev *dev);
          int  (*enable_wake) (struct pci_dev *dev, u32 state, int enable);
      };
    • pci_dev

      这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等:

      struct pci_dev {
          struct list_head global_list;
          struct list_head bus_list;
          struct pci_bus  *bus;
          struct pci_bus  *subordinate;
          void        *sysdata;
          struct proc_dir_entry *procent;
          unsigned int    devfn;
          unsigned short  vendor;
          unsigned short  device;
          unsigned short  subsystem_vendor;
          unsigned short  subsystem_device;
          unsigned int    class;
          u8      hdr_type;
          u8      rom_base_reg;
          struct pci_driver *driver;
          void        *driver_data;
          u64     dma_mask;
          u32             current_state;
          unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];
          unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];
          unsigned int    irq;
          struct resource resource[DEVICE_COUNT_RESOURCE];
          struct resource dma_resource[DEVICE_COUNT_DMA];
          struct resource irq_resource[DEVICE_COUNT_IRQ];
          char        name[80];
          char        slot_name[8];
          int     active;
          int     ro;
          unsigned short  regs;
          int (*prepare)(struct pci_dev *dev);
          int (*activate)(struct pci_dev *dev);
          int (*deactivate)(struct pci_dev *dev);
      };

    2. 基本框架

    在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块。下面给出一个典型的PCI设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。

    /* 指明该驱动程序适用于哪一些PCI设备 */
    static struct pci_device_id demo_pci_tbl [] __initdata = {
        {PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO,
         PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO},
        {0,}
    };
    /* 对特定PCI设备进行描述的数据结构 */
    struct demo_card {
        unsigned int magic;
        /* 使用链表保存所有同类的PCI设备 */
        struct demo_card *next;
        
        /* ... */
    }
    /* 中断处理模块 */
    static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    {
        /* ... */
    }
    /* 设备文件操作接口 */
    static struct file_operations demo_fops = {
        owner:      THIS_MODULE,   /* demo_fops所属的设备模块 */
        read:       demo_read,    /* 读设备操作*/
        write:      demo_write,    /* 写设备操作*/
        ioctl:      demo_ioctl,    /* 控制设备操作*/
        mmap:       demo_mmap,    /* 内存重映射操作*/
        open:       demo_open,    /* 打开设备操作*/
        release:    demo_release    /* 释放设备操作*/
        /* ... */
    };
    /* 设备模块信息 */
    static struct pci_driver demo_pci_driver = {
        name:       demo_MODULE_NAME,    /* 设备模块名称 */
        id_table:   demo_pci_tbl,    /* 能够驱动的设备列表 */
        probe:      demo_probe,    /* 查找并初始化设备 */
        remove:     demo_remove    /* 卸载设备模块 */
        /* ... */
    };
    static int __init demo_init_module (void)
    {
        /* ... */
    }
    static void __exit demo_cleanup_module (void)
    {
        pci_unregister_driver(&demo_pci_driver);
    }
    /* 加载驱动程序模块入口 */
    module_init(demo_init_module);
    /* 卸载驱动程序模块入口 */
    module_exit(demo_cleanup_module);

     

    上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。需要注意的是,同加载和卸载模块相关的函数或数据结构都要在前面加上__init、__exit等标志符,以使同普通函数区分开来。构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。

    3. 初始化设备模块

    在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:

    • 检查PCI总线是否被Linux内核支持;
    • 检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。
    • 读出配置头中的信息提供给驱动程序使用。

    当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,一般都会调用如下的代码:

    static int __init demo_init_module (void)
    {
        /* 检查系统是否支持PCI总线 */
        if (!pci_present())
            return -ENODEV;
        /* 注册硬件驱动程序 */
        if (!pci_register_driver(&demo_pci_driver)) {
            pci_unregister_driver(&demo_pci_driver);
                    return -ENODEV;
        }
        /* ... */
       
        return 0;
    }

     

    驱动程序首先调用函数pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了。在2.4以前的内核中,需要手工调用pci_find_device( )函数来查找PCI设备,但在2.4以后更好的办法是调用pci_register_driver( )函数来注册PCI设备的驱动程序,此时需要提供一个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。

    static int __init demo_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id)
    {
        struct demo_card *card;
        /* 启动PCI设备 */
        if (pci_enable_device(pci_dev))
            return -EIO;
        /* 设备DMA标识 */
        if (pci_set_dma_mask(pci_dev, DEMO_DMA_MASK)) {
            return -ENODEV;
        }
        /* 在内核空间中动态申请内存 */
        if ((card = kmalloc(sizeof(struct demo_card), GFP_KERNEL)) == NULL) {
            printk(KERN_ERR "pci_demo: out of memory\n");
            return -ENOMEM;
        }
        memset(card, 0, sizeof(*card));
        /* 读取PCI配置信息 */
        card->iobase = pci_resource_start (pci_dev, 1);
        card->pci_dev = pci_dev;
        card->pci_id = pci_id->device;
        card->irq = pci_dev->irq;
        card->next = devs;
        card->magic = DEMO_CARD_MAGIC;
        /* 设置成总线主DMA模式 */    
        pci_set_master(pci_dev);
        /* 申请I/O资源 */
        request_region(card->iobase, 64, card_names[pci_id->driver_data]);
        return 0;
    }

     

    4. 打开设备模块

    在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。

    static int demo_open(struct inode *inode, struct file *file)
    {
        /* 申请中断,注册中断处理程序 */
        request_irq(card->irq, &demo_interrupt, SA_SHIRQ,
            card_names[pci_id->driver_data], card)) {
        /* 检查读写模式 */
        if(file->f_mode & FMODE_READ) {
            /* ... */
        }
        if(file->f_mode & FMODE_WRITE) {
           /* ... */
        }
        
        /* 申请对设备的控制权 */
        down(&card->open_sem);
        while(card->open_mode & file->f_mode) {
            if (file->f_flags & O_NONBLOCK) {
                /* NONBLOCK模式,返回-EBUSY */
                up(&card->open_sem);
                return -EBUSY;
            } else {
                /* 等待调度,获得控制权 */
                card->open_mode |= f_mode & (FMODE_READ | FMODE_WRITE);
                up(&card->open_sem);
                /* 设备打开计数增1 */
                MOD_INC_USE_COUNT;
                /* ... */
            }
        }
    }

     

    5. 数据读写和控制信息模块

    PCI设备驱动程序可以通过demo_fops 结构中的函数demo_ioctl( ),向应用程序提供对硬件进行控制的接口。例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里:

    static int demo_ioctl(struct inode *inode, struct file *file,
          unsigned int cmd, unsigned long arg)
    {
        /* ... */
        
        switch(cmd) {
            case DEMO_RDATA:
                /* 从I/O端口读取4字节的数据 */
                val = inl(card->iobae + 0x10);
                
    /* 将读取的数据传输到用户空间 */
                return 0;
        }
        
        /* ... */
    }

     

    事实上,在demo_fops里还可以实现诸如demo_read( )、demo_mmap( )等操作,Linux内核源码中的driver目录里提供了许多设备驱动程序的源代码,找那里可以找到类似的例子。在对资源的访问方式上,除了有I/O指令以外,还有对外设I/O内存的访问。对这些内存的操作一方面可以通过把I/O内存重新映射后作为普通内存进行操作,另一方面也可以通过总线主DMA(Bus Master DMA)的方式让设备把数据通过DMA传送到系统内存中。

    6. 中断处理模块

    PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理。

    static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    {
        struct demo_card *card = (struct demo_card *)dev_id;
        u32 status;
        spin_lock(&card->lock);
        /* 识别中断 */
        status = inl(card->iobase + GLOB_STA);
        if(!(status & INT_MASK)) 
        {
            spin_unlock(&card->lock);
            return;  /* not for us */
        }
        /* 告诉设备已经收到中断 */
        outl(status & INT_MASK, card->iobase + GLOB_STA);
        spin_unlock(&card->lock);
        
        /* 其它进一步的处理,如更新DMA缓冲区指针等 */
    }

     

    7. 释放设备模块

    释放设备模块主要负责释放对设备的控制权,释放占用的内存和中断等,所做的事情正好与打开设备模块相反:

    static int demo_release(struct inode *inode, struct file *file)
    {
        /* ... */
        
        /* 释放对设备的控制权 */
        card->open_mode &= (FMODE_READ | FMODE_WRITE);
        
        /* 唤醒其它等待获取控制权的进程 */
        wake_up(&card->open_wait);
        up(&card->open_sem);
        
        /* 释放中断 */
        free_irq(card->irq, card);
        
        /* 设备打开计数增1 */
        MOD_DEC_USE_COUNT;
        
        /* ... */  
    }

     

    8. 卸载设备模块

    卸载设备模块与初始化设备模块是相对应的,实现起来相对比较简单,主要是调用函数pci_unregister_driver( )从Linux内核中注销设备驱动程序:

    static void __exit demo_cleanup_module (void)
    {
        pci_unregister_driver(&demo_pci_driver);
    }
    •  如何将驱动程序编译后加载进内核

    (1)编写Makefile文件

    makefile文件实例

    ifneq ($(KERNELRELEASE),)
    obj-m:=hello.o
    else
    #generate the path
    CURRENT_PATH:=$(shell pwd)
    #the absolute path
    LINUX_KERNEL_PATH:=/lib/modules/$(shell uname -r)/build
    #complie object
    default:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    clean:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
    endif

    obj-m 表示该文件要作为模块编译  obj-y则表示该文件要编译进内核

    正常情况下只需修改hello.o即可

    (2)执行make命令生成 *.ko 文件

    (3)sudo insmod *.ko加载驱动模块

    (4)sudo rmmod *.ko卸载驱动模块

    (5)使用dmesg | tail -10来查看内核输出的最后十条信息

    (6)使用modinfo *.ko来查看模块信息

    • PCI IO/内存地址区域

    一个PCI设备可实现多达6个I/O地址区域,每个区域既可以使内存也可以是I/O地址。在内核中PCI设备的I/O区域已经被集成到通用资源管理器。因此,我们无需访问配置变量来了解设备被映射到内存或者I/O空间的何处。获得区域信息的首选接口是下面的宏定义:

    #define pci_resource_start(dev, bar)((dev)->resource[(bar)].start)

     该宏返回六个PCI I/O区域之一的首地址(内存地址或者I/O端口号).该区域由整数的bar(base address register,基地址寄存器)指定,bar取值为0到5。

    #define pci_resource_end(dev, bar)((dev)->resource[(bar)].end)

     该宏返回第bar个I/O区域的首地址。注意这是最后一个可用的地址,而不是该区域之后的第一个地址。

    #define pci_resource_flags(dev, bar)((dev)->resource[(bar)].flags)

     该宏返回和该资源相关联的标志。资源标志用来定义单个资源的特性,对与PCI I/O区域相关的PCI资源,该信息从基地址寄存器中获得,但对于和PCI无关的资源,它可能来自其他地方。所有资源标志定义在<linux/ioport.h>。

    • PCIE驱动下的DMA及中断

    (1)DMA循环缓冲区的分配与实现:

           对于高速数据信号的采集处理,需要在驱动程序的初始化模块(probe)中申请大量的DMA循环缓冲区,申请的大小直接关系着能否实时对高速数据处理的成败。直接内存访问(DMA)是一种硬件机制,允许外围设备和主内存直接直接传输I/O数据,避免了大量的计算开销。

    (2) Linux内核的内存分区段:

           三个区段,可用于DMA的内存,常规内存以及高端内存。

    ·        通常的内存分配发生在常规内存区,但是通过设置内存标识也可以请求在其他区段中分配。可用于DMA的内存指存在于特别地址范围内的内存,外设可以利用这些内存执行DMA访问,进行数据通信传输。

    ·        DMA循环缓冲区的分配要求:物理连续,DMA可以访问,足够大。

    (3)Linux内存分配函数:

    ·        Linux系统使用虚拟地址,内存分配函数提供的都是虚拟地址,通过virt_to_bus转换才能得到物理地址。

    ·        分配内核内存空间的函数:kmalloc实现小于128KB的内核内存申请,申请空间物理连续;__get_free_pages实现最大4MB的内存申请,以页为单位,所申请空间物理连续;vmalloc分配的虚拟地址空间连续,但在物理上可能不连续。

    ·        Linux内核中专门提供了用于PCI设备申请内核内存的函数pci_alloc_consistent,支持按字节长度申请,该函数调用__get_free_pages,故一次最大为4MB。

    (4) DMA数据传输的方式:

    ·        一种是软件发起的数据请求(例如通过read函数调用),另一种是硬件异步将数据传给系统。对于数据采集设备,即便没有进程去读取数据,也要不断写入,随时等待进程调用,因此驱动程序应该维护一个环形缓冲区,当read调用时可以随时返回给用户空间需要的数据。

    (5)PCIe中向CPU发起中断请求的方式:

    ·        消息信号中断(MSI),INTx中断。

    ·        在MSI中断方式下,设备通过向OS预先分配的主存空间写入特定数据的方式请求CPU的中断服务,为PCIe系统首选的中断信号机制,对于PCIe到PCI/PCI-X的桥接设备和不能使用MSI机制的传统端点设备,采用INTx虚拟中断机制。

    ·        PCIe设备注册中断时使用共享中断方式,Linux系统通过request_irq实现中断处理程序的注册,调用位置在设备第一次打开,硬件产生中断之前;同样,free_irq时机在最后一次关闭设备,硬件不用中断处理器之后。

    ·        中断处理函数的功能是将有关中断接收的信息反馈给设备,并对数据进行相应读写。中断信号到来,系统调用相应的中断处理函数,函数判断中断号是否匹配,若是,则清除中断寄存器相应的位,即在驱动程序发起新的DMA之前设备不会产生其他中断,然后进行相应处理。

    (6)数据读写和ioctl控制:

    ·        数据读写:应用进程不需要数据时,驱动程序动态维护DMA环形缓冲区,当应用进程请求数据,驱动程序通过Linux内核提供copy_from_user()/copy_to_user()实现内核态和用户态之间的数据拷贝。

    ·        硬件控制:用户空间经常回去请求设备锁门,报告错误信息,设置寄存器等,这些操作都通过ioctl支持,可以对PCIe卡给定的寄存器空间进行配置。

    (7)中断处理程序的注册:

    ·        中断号在BIOS初始化阶段分配并写入设备配置空间,然后Linux在建立pci_dev时从配置空间中读出该中断号并写入pci_dev的irq成员中,所以注册中断程序时直接从pci_dev中读取就行。

    ·        当设备发生中断,8259A将中断号发给CPU,CPU根据中断号找到中断处理程序,执行。

    (8)DMA数据传输机制的产生:

    ·        传统经典过程:数据到达网卡 -> 网卡产生一个中断给内核 -> 内核使用 I/O 指令,从网卡I/O区域中去读取数据。这种方式,当大流量数据到来时,网卡会产生大量中断,内核在中断上下文中,会浪费大量资源处理中断本身。

    ·        改进:NAPI,即轮询,即内核屏蔽中断,隔一定时间去问网卡,是否有数据。则在数据量小的情况下,这种方式会浪费大量资源。

    ·        另一个问题,CPU到网卡的I/O区域,包括I/O寄存器和I/O内存中读取,再放到系统物理内存,都占用大量CPU资源,做改进,即有了DMA,让网卡直接从主内存之间读写自己的I/O数据。

    ·        首先,内核在主内存中为收发数据建立一个环形的缓冲队列(DMA环形缓冲区),内核将这个缓冲区通过DMA映射,将这个队列交给网卡;网卡收到数据,直接放进环形缓冲区,即直接放到主内存,然后向系统产生中断;

    ·        内核收到中断,取消DMA映射,可以直接从主内存中读取数据。

    展开全文
  • linux PCIE驱动开发

    千次阅读 2018-01-24 18:48:00
    Example #2: My company offers both PCI and PCIe versions of a peripheral board, and they use the same Windows/Linux driver package. The driver does not "know" the difference between the two boards. ...
    •  linux下PCI驱动源码实例1,该源码缺少pci_fops的初始化
    #include <linux/fs.h>
    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/pci.h>
    #include <linux/interrupt.h>
    #include <asm-generic/signal.h>
    #undef debug
    
    
    // ATTENTION copied from /uboot_for_mpc/arch/powerpc/include/asm/signal.h
    // Maybe it don't work with that
    //____________________________________________________________
    #define SA_INTERRUPT    0x20000000 /* dummy -- ignored */
    #define SA_SHIRQ        0x04000000
    //____________________________________________________________
    
    #define pci_module_init pci_register_driver // function is obsoleted
    
    // Hardware specific part
    #define MY_VENDOR_ID 0x5333
    #define MY_DEVICE_ID 0x8e40
    #define MAJOR_NR     240
    #define DRIVER_NAME  "PCI-Driver"
    
    static unsigned long ioport=0L, iolen=0L, memstart=0L, memlen=0L,flag0,flag1,flag2,temp=0L;
    
    // private_data
    struct _instance_data {
    
        int counter; // just as a example (5-27)
    
        // other instance specific data
    };
    
    // Interrupt Service Routine
    static irqreturn_t pci_isr( int irq, void *dev_id, struct pt_regs *regs )
    {
        return IRQ_HANDLED;
    }
    
    
    // Check if this driver is for the new device
    static int device_init(struct pci_dev *dev,
            const struct pci_device_id *id)
    {
        int err=0;  // temp variable
    
        #ifdef debug
    
        flag0=pci_resource_flags(dev, 0 );
        flag1=pci_resource_flags(dev, 1 );
        flag2=pci_resource_flags(dev, 2 );
        printk("DEBUG: FLAGS0 = %u\n",flag0);
        printk("DEBUG: FLAGS1 = %u\n",flag1);
        printk("DEBUG: FLAGS2 = %u\n",flag2);
    
        /*
         * The following sequence checks if the resource is in the
         * IO / Storage / Interrupt / DMA address space
         * and prints the result in the dmesg log
         */
        if(pci_resource_flags(dev,0) & IORESOURCE_IO)
        {
            // Ressource is in the IO address space
            printk("DEBUG: IORESOURCE_IO\n");
        }
        else if (pci_resource_flags(dev,0) & IORESOURCE_MEM)
        {
            // Resource is in the Storage address space
            printk("DEBUG: IORESOURCE_MEM\n");
        }
        else if (pci_resource_flags(dev,0) & IORESOURCE_IRQ)
        {
            // Resource is in the IRQ address space
            printk("DEBUG: IORESOURCE_IRQ\n");
        }
        else if (pci_resource_flags(dev,0) & IORESOURCE_DMA)
        {
            // Resource is in the DMA address space
            printk("DEBUG: IORESOURCE_DMA\n");
        }
        else
        {
            printk("DEBUG: NOTHING\n");
        }
    
        #endif /* debug */
    
        // allocate memory_region
        memstart = pci_resource_start( dev, 0 );
        memlen = pci_resource_len( dev, 0 );
        if( request_mem_region( memstart, memlen, dev->dev.kobj.name )==NULL ) {
            printk(KERN_ERR "Memory address conflict for device \"%s\"\n",
                    dev->dev.kobj.name);
            return -EIO;
        }
        // allocate a interrupt
        if(request_irq(dev->irq,pci_isr,SA_INTERRUPT|SA_SHIRQ,
                "pci_drv",dev)) {
            printk( KERN_ERR "pci_drv: IRQ %d not free.\n", dev->irq );
        }
        else
        {
            err=pci_enable_device( dev );
            if(err==0)      // enable device successful
            {
                return 0;
            }
            else        // enable device not successful
            {
                return err;
            }
    
        }
        // cleanup_mem
        release_mem_region( memstart, memlen );
        return -EIO;
    }
    // Function for deinitialization of the device
    static void device_deinit( struct pci_dev *pdev )
    {
        free_irq( pdev->irq, pdev );
        if( memstart )
            release_mem_region( memstart, memlen );
    }
    
    static struct file_operations pci_fops;
    
    static struct pci_device_id pci_drv_tbl[] __devinitdata = {
        {       MY_VENDOR_ID,           // manufacturer identifier
            MY_DEVICE_ID,           // device identifier
            PCI_ANY_ID,             // subsystem manufacturer identifier
            PCI_ANY_ID,             // subsystem device identifier
            0,                      // device class
            0,                      // mask for device class
            0 },                    // driver specific data
            { 0, }
    };
    
    static int driver_open( struct inode *geraetedatei, struct file *instance )
    {
        struct _instance_data *iptr;
    
        iptr = (struct _instance_data *)kmalloc(sizeof(struct _instance_data),
                GFP_KERNEL);
        if( iptr==0 ) {
            printk("not enough kernel mem\n");
            return -ENOMEM;
        }
        /* replace the following line with your instructions  */
        iptr->counter= strlen("Hello World\n")+1;    // just as a example (5-27)
    
        instance->private_data = (void *)iptr;
        return 0;
    }
    
    static void driver_close( struct file *instance )
    {
        if( instance->private_data )
            kfree( instance->private_data );
    }
    
    
    static struct pci_driver pci_drv = {
        .name= "pci_drv",
                .id_table= pci_drv_tbl,
                .probe= device_init,
                .remove= device_deinit,
    };
    
    static int __init pci_drv_init(void)
    {    // register the driver by the OS
        if(register_chrdev(MAJOR_NR, DRIVER_NAME, &pci_fops)==0) {
            if(pci_module_init(&pci_drv) == 0 ) // register by the subsystem
                return 0;
            unregister_chrdev(MAJOR_NR,DRIVER_NAME); // unregister if no subsystem support
        }
        return -EIO;
    }
    
    static void __exit pci_drv_exit(void)
    {
        pci_unregister_driver( &pci_drv );
        unregister_chrdev(MAJOR_NR,DRIVER_NAME);
    }
    
    module_init(pci_drv_init);
    module_exit(pci_drv_exit);
    
    MODULE_LICENSE("GPL");

     

      

    •  PCIE同PCI驱动的差异

    From a software standpoint, PCI and PCI Express devices are essentially the same. PCIe devices had the same configuration space, BARs, and (usually) support the same PCI INTx interrupts.一般情况下,两者基本保持一致

    Example #1: Windows XP has no special knowledge of PCIe, but runs fine on PCIe systems.

    Example #2: My company offers both PCI and PCIe versions of a peripheral board, and they use the same Windows/Linux driver package. The driver does not "know" the difference between the two boards.

    However: PCIe devices frequently take advantage of "advanced" features, like MSI, Hotplugging, extended configuration space, etc. Many of these feature existed on legacy PCI, but were unused. If this is a device you are designing, it is up to you whether or not you implement these advanced features.但是pcie在一些高级特性上有优势,比如MSI(Message Signaled Interrupts)、Hotplugging(热插拔)、配置空间扩展等。

    • linux设备驱动程序框架

    Linux将所有外部设备看成是一类特殊文件,称之为“设备文件”,如果说系统调用是Linux内核和应用程序之间的接口,那么设备驱动程序则可以看成是Linux内核与外部设备之间的接口。设备驱动程序向应用程序屏蔽了硬件在实现上的细节,使得应用程序可以像操作普通文件一样来操作外部设备。

    1. 字符设备和块设备

    Linux抽象了对硬件的处理,所有的硬件设备都可以像普通文件一样来看待:它们可以使用和操作文件相同的、标准的系统调用接口来完成打开、关闭、读写和I/O控制操作,而驱动程序的主要任务也就是要实现这些系统调用函数。Linux系统中的所有硬件设备都使用一个特殊的设备文件来表示,例如,系统中的第一个IDE硬盘使用/dev/hda表示。每个设备文件对应有两个设备号:一个是主设备号,标识该设备的种类,也标识了该设备所使用的驱动程序;另一个是次设备号,标识使用同一设备驱动程序的不同硬件设备。设备文件的主设备号必须与设备驱动程序在登录该设备时申请的主设备号一致,否则用户进程将无法访问到设备驱动程序。

    在Linux操作系统下有两类主要的设备文件:一类是字符设备,另一类则是块设备。字符设备是以字节为单位逐个进行I/O操作的设备,在对字符设备发出读写请求时,实际的硬件I/O紧接着就发生了,一般来说字符设备中的缓存是可有可无的,而且也不支持随机访问。块设备则是利用一块系统内存作为缓冲区,当用户进程对设备进行读写请求时,驱动程序先查看缓冲区中的内容,如果缓冲区中的数据能满足用户的要求就返回相应的数据,否则就调用相应的请求函数来进行实际的I/O操作。块设备主要是针对磁盘等慢速设备设计的,其目的是避免耗费过多的CPU时间来等待操作的完成。一般说来,PCI卡通常都属于字符设备。

    所有已经注册(即已经加载了驱动程序)的硬件设备的主设备号可以从/proc/devices文件中得到。使用mknod命令可以创建指定类型的设备文件,同时为其分配相应的主设备号和次设备号。例如,下面的命令:

    [root@gary root]# mknod  /dev/lp0  c  6  0

     

    将建立一个主设备号为6,次设备号为0的字符设备文件/dev/lp0。当应用程序对某个设备文件进行系统调用时,Linux内核会根据该设备文件的设备类型和主设备号调用相应的驱动程序,并从用户态进入到核心态,再由驱动程序判断该设备的次设备号,最终完成对相应硬件的操作。

    2. 设备驱动程序接口

    Linux中的I/O子系统向内核中的其他部分提供了一个统一的标准设备接口,这是通过include/linux/fs.h中的数据结构file_operations来完成的:

    struct file_operations {
            struct module *owner;
            loff_t (*llseek) (struct file *, loff_t, int);
            ssize_t (*read) (struct file *, char *, size_t, loff_t *);
            ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
            int (*readdir) (struct file *, void *, filldir_t);
            unsigned int (*poll) (struct file *, struct poll_table_struct *);
            int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
            int (*mmap) (struct file *, struct vm_area_struct *);
            int (*open) (struct inode *, struct file *);
            int (*flush) (struct file *);
            int (*release) (struct inode *, struct file *);
            int (*fsync) (struct file *, struct dentry *, int datasync);
            int (*fasync) (int, struct file *, int);
            int (*lock) (struct file *, int, struct file_lock *);
            ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
            ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
            ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
            unsigned long (*get_unmapped_area)(struct file *, unsigned long, 
             unsigned long, unsigned long, unsigned long);
    };

     

    当应用程序对设备文件进行诸如open、close、read、write等操作时,Linux内核将通过file_operations结构访问驱动程序提供的函数。例如,当应用程序对设备文件执行读操作时,内核将调用file_operations结构中的read函数。

    3. 设备驱动程序模块

    Linux下的设备驱动程序可以按照两种方式进行编译,一种是直接静态编译成内核的一部分,另一种则是编译成可以动态加载的模块。如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态地卸载,不利于调试,所有推荐使用模块方式。

    从本质上来讲,模块也是内核的一部分,它不同于普通的应用程序,不能调用位于用户态下的C或者C++库函数,而只能调用Linux内核提供的函数,在/proc/ksyms中可以查看到内核提供的所有函数。

    在以模块方式编写驱动程序时,要实现两个必不可少的函数init_module( )和cleanup_module( ),而且至少要包含<linux/krernel.h>和<linux/module.h>两个头文件。在用gcc编译内核模块时,需要加上-DMODULE -D__KERNEL__ -DLINUX这几个参数,编译生成的模块(一般为.o文件)可以使用命令insmod载入Linux内核,从而成为内核的一个组成部分,此时内核会调用模块中的函数init_module( )。当不需要该模块时,可以使用rmmod命令进行卸载,此进内核会调用模块中的函数cleanup_module( )。任何时候都可以使用命令来lsmod查看目前已经加载的模块以及正在使用该模块的用户数。

    4. 设备驱动程序结构

    了解设备驱动程序的基本结构(或者称为框架),对开发人员而言是非常重要的,Linux的设备驱动程序大致可以分为如下几个部分:驱动程序的注册与注销、设备的打开与释放、设备的读写操作、设备的控制操作、设备的中断和轮询处理。

    • 驱动程序的注册与注销

      向系统增加一个驱动程序意味着要赋予它一个主设备号,这可以通过在驱动程序的初始化过程中调用register_chrdev( )或者register_blkdev( )来完成。而在关闭字符设备或者块设备时,则需要通过调用unregister_chrdev( )或unregister_blkdev( )从内核中注销设备,同时释放占用的主设备号。

    • 设备的打开与释放

      打开设备是通过调用file_operations结构中的函数open( )来完成的,它是驱动程序用来为今后的操作完成初始化准备工作的。在大部分驱动程序中,open( )通常需要完成下列工作:

      1. 检查设备相关错误,如设备尚未准备好等。
      2. 如果是第一次打开,则初始化硬件设备。
      3. 识别次设备号,如果有必要则更新读写操作的当前位置指针f_ops。
      4. 分配和填写要放在file->private_data里的数据结构。
      5. 使用计数增1。

      释放设备是通过调用file_operations结构中的函数release( )来完成的,这个设备方法有时也被称为close( ),它的作用正好与open( )相反,通常要完成下列工作:

      1. 使用计数减1。
      2. 释放在file->private_data中分配的内存。
      3. 如果使用计算为0,则关闭设备。
    • 设备的读写操作

      字符设备的读写操作相对比较简单,直接使用函数read( )和write( )就可以了。但如果是块设备的话,则需要调用函数block_read( )和block_write( )来进行数据读写,这两个函数将向设备请求表中增加读写请求,以便Linux内核可以对请求顺序进行优化。由于是对内存缓冲区而不是直接对设备进行操作的,因此能很大程度上加快读写速度。如果内存缓冲区中没有所要读入的数据,或者需要执行写操作将数据写入设备,那么就要执行真正的数据传输,这是通过调用数据结构blk_dev_struct中的函数request_fn( )来完成的。

    • 设备的控制操作

      除了读写操作外,应用程序有时还需要对设备进行控制,这可以通过设备驱动程序中的函数ioctl( )来完成。ioctl( )的用法与具体设备密切关联,因此需要根据设备的实际情况进行具体分析。

    • 设备的中断和轮询处理 

      对于不支持中断的硬件设备,读写时需要轮流查询设备状态,以便决定是否继续进行数据传输。如果设备支持中断,则可以按中断方式进行操作。

    三、PCI驱动程序实现

    1. 关键数据结构

    PCI设备上有三种地址空间:PCI的I/O空间、PCI的存储空间和PCI的配置空间。CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的PCI初始化代码使用。内核在启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号以及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。

    Linux驱动程序通常使用结构(struct)来表示一种设备,而结构体中的变量则代表某一具体设备,该变量存放了与该设备相关的所有信息。好的驱动程序都应该能驱动多个同种设备,每个设备之间用次设备号进行区分,如果采用结构数据来代表所有能由该驱动程序驱动的设备,那么就可以简单地使用数组下标来表示次设备号。

    在PCI驱动程序中,下面几个关键数据结构起着非常核心的作用:

    • pci_driver

      这个数据结构在文件include/linux/pci.h里,这是Linux内核版本2.4之后为新型的PCI设备驱动程序所添加的,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe( )和卸载设备的函数remove( ):

      struct pci_driver {
          struct list_head node;
          char *name;
          const struct pci_device_id *id_table;
          int  (*probe)  (struct pci_dev *dev, const struct pci_device_id *id);
          void (*remove) (struct pci_dev *dev);
          int  (*save_state) (struct pci_dev *dev, u32 state);
          int  (*suspend)(struct pci_dev *dev, u32 state);
          int  (*resume) (struct pci_dev *dev);
          int  (*enable_wake) (struct pci_dev *dev, u32 state, int enable);
      };
    • pci_dev

      这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等:

      struct pci_dev {
          struct list_head global_list;
          struct list_head bus_list;
          struct pci_bus  *bus;
          struct pci_bus  *subordinate;
          void        *sysdata;
          struct proc_dir_entry *procent;
          unsigned int    devfn;
          unsigned short  vendor;
          unsigned short  device;
          unsigned short  subsystem_vendor;
          unsigned short  subsystem_device;
          unsigned int    class;
          u8      hdr_type;
          u8      rom_base_reg;
          struct pci_driver *driver;
          void        *driver_data;
          u64     dma_mask;
          u32             current_state;
          unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];
          unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];
          unsigned int    irq;
          struct resource resource[DEVICE_COUNT_RESOURCE];
          struct resource dma_resource[DEVICE_COUNT_DMA];
          struct resource irq_resource[DEVICE_COUNT_IRQ];
          char        name[80];
          char        slot_name[8];
          int     active;
          int     ro;
          unsigned short  regs;
          int (*prepare)(struct pci_dev *dev);
          int (*activate)(struct pci_dev *dev);
          int (*deactivate)(struct pci_dev *dev);
      };

    2. 基本框架

    在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块。下面给出一个典型的PCI设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。

    /* 指明该驱动程序适用于哪一些PCI设备 */
    static struct pci_device_id demo_pci_tbl [] __initdata = {
        {PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO,
         PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO},
        {0,}
    };
    /* 对特定PCI设备进行描述的数据结构 */
    struct demo_card {
        unsigned int magic;
        /* 使用链表保存所有同类的PCI设备 */
        struct demo_card *next;
        
        /* ... */
    }
    /* 中断处理模块 */
    static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    {
        /* ... */
    }
    /* 设备文件操作接口 */
    static struct file_operations demo_fops = {
        owner:      THIS_MODULE,   /* demo_fops所属的设备模块 */
        read:       demo_read,    /* 读设备操作*/
        write:      demo_write,    /* 写设备操作*/
        ioctl:      demo_ioctl,    /* 控制设备操作*/
        mmap:       demo_mmap,    /* 内存重映射操作*/
        open:       demo_open,    /* 打开设备操作*/
        release:    demo_release    /* 释放设备操作*/
        /* ... */
    };
    /* 设备模块信息 */
    static struct pci_driver demo_pci_driver = {
        name:       demo_MODULE_NAME,    /* 设备模块名称 */
        id_table:   demo_pci_tbl,    /* 能够驱动的设备列表 */
        probe:      demo_probe,    /* 查找并初始化设备 */
        remove:     demo_remove    /* 卸载设备模块 */
        /* ... */
    };
    static int __init demo_init_module (void)
    {
        /* ... */
    }
    static void __exit demo_cleanup_module (void)
    {
        pci_unregister_driver(&demo_pci_driver);
    }
    /* 加载驱动程序模块入口 */
    module_init(demo_init_module);
    /* 卸载驱动程序模块入口 */
    module_exit(demo_cleanup_module);

     

    上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。需要注意的是,同加载和卸载模块相关的函数或数据结构都要在前面加上__init、__exit等标志符,以使同普通函数区分开来。构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。

    3. 初始化设备模块

    在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:

    • 检查PCI总线是否被Linux内核支持;
    • 检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。
    • 读出配置头中的信息提供给驱动程序使用。

    当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,一般都会调用如下的代码:

    static int __init demo_init_module (void)
    {
        /* 检查系统是否支持PCI总线 */
        if (!pci_present())
            return -ENODEV;
        /* 注册硬件驱动程序 */
        if (!pci_register_driver(&demo_pci_driver)) {
            pci_unregister_driver(&demo_pci_driver);
                    return -ENODEV;
        }
        /* ... */
       
        return 0;
    }

     

    驱动程序首先调用函数pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了。在2.4以前的内核中,需要手工调用pci_find_device( )函数来查找PCI设备,但在2.4以后更好的办法是调用pci_register_driver( )函数来注册PCI设备的驱动程序,此时需要提供一个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。

    static int __init demo_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id)
    {
        struct demo_card *card;
        /* 启动PCI设备 */
        if (pci_enable_device(pci_dev))
            return -EIO;
        /* 设备DMA标识 */
        if (pci_set_dma_mask(pci_dev, DEMO_DMA_MASK)) {
            return -ENODEV;
        }
        /* 在内核空间中动态申请内存 */
        if ((card = kmalloc(sizeof(struct demo_card), GFP_KERNEL)) == NULL) {
            printk(KERN_ERR "pci_demo: out of memory\n");
            return -ENOMEM;
        }
        memset(card, 0, sizeof(*card));
        /* 读取PCI配置信息 */
        card->iobase = pci_resource_start (pci_dev, 1);
        card->pci_dev = pci_dev;
        card->pci_id = pci_id->device;
        card->irq = pci_dev->irq;
        card->next = devs;
        card->magic = DEMO_CARD_MAGIC;
        /* 设置成总线主DMA模式 */    
        pci_set_master(pci_dev);
        /* 申请I/O资源 */
        request_region(card->iobase, 64, card_names[pci_id->driver_data]);
        return 0;
    }

     

    4. 打开设备模块

    在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。

    static int demo_open(struct inode *inode, struct file *file)
    {
        /* 申请中断,注册中断处理程序 */
        request_irq(card->irq, &demo_interrupt, SA_SHIRQ,
            card_names[pci_id->driver_data], card)) {
        /* 检查读写模式 */
        if(file->f_mode & FMODE_READ) {
            /* ... */
        }
        if(file->f_mode & FMODE_WRITE) {
           /* ... */
        }
        
        /* 申请对设备的控制权 */
        down(&card->open_sem);
        while(card->open_mode & file->f_mode) {
            if (file->f_flags & O_NONBLOCK) {
                /* NONBLOCK模式,返回-EBUSY */
                up(&card->open_sem);
                return -EBUSY;
            } else {
                /* 等待调度,获得控制权 */
                card->open_mode |= f_mode & (FMODE_READ | FMODE_WRITE);
                up(&card->open_sem);
                /* 设备打开计数增1 */
                MOD_INC_USE_COUNT;
                /* ... */
            }
        }
    }

     

    5. 数据读写和控制信息模块

    PCI设备驱动程序可以通过demo_fops 结构中的函数demo_ioctl( ),向应用程序提供对硬件进行控制的接口。例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里:

    static int demo_ioctl(struct inode *inode, struct file *file,
          unsigned int cmd, unsigned long arg)
    {
        /* ... */
        
        switch(cmd) {
            case DEMO_RDATA:
                /* 从I/O端口读取4字节的数据 */
                val = inl(card->iobae + 0x10);
                
    /* 将读取的数据传输到用户空间 */
                return 0;
        }
        
        /* ... */
    }

     

    事实上,在demo_fops里还可以实现诸如demo_read( )、demo_mmap( )等操作,Linux内核源码中的driver目录里提供了许多设备驱动程序的源代码,找那里可以找到类似的例子。在对资源的访问方式上,除了有I/O指令以外,还有对外设I/O内存的访问。对这些内存的操作一方面可以通过把I/O内存重新映射后作为普通内存进行操作,另一方面也可以通过总线主DMA(Bus Master DMA)的方式让设备把数据通过DMA传送到系统内存中。

    6. 中断处理模块

    PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理。

    static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    {
        struct demo_card *card = (struct demo_card *)dev_id;
        u32 status;
        spin_lock(&card->lock);
        /* 识别中断 */
        status = inl(card->iobase + GLOB_STA);
        if(!(status & INT_MASK)) 
        {
            spin_unlock(&card->lock);
            return;  /* not for us */
        }
        /* 告诉设备已经收到中断 */
        outl(status & INT_MASK, card->iobase + GLOB_STA);
        spin_unlock(&card->lock);
        
        /* 其它进一步的处理,如更新DMA缓冲区指针等 */
    }

     

    7. 释放设备模块

    释放设备模块主要负责释放对设备的控制权,释放占用的内存和中断等,所做的事情正好与打开设备模块相反:

    static int demo_release(struct inode *inode, struct file *file)
    {
        /* ... */
        
        /* 释放对设备的控制权 */
        card->open_mode &= (FMODE_READ | FMODE_WRITE);
        
        /* 唤醒其它等待获取控制权的进程 */
        wake_up(&card->open_wait);
        up(&card->open_sem);
        
        /* 释放中断 */
        free_irq(card->irq, card);
        
        /* 设备打开计数增1 */
        MOD_DEC_USE_COUNT;
        
        /* ... */  
    }

     

    8. 卸载设备模块

    卸载设备模块与初始化设备模块是相对应的,实现起来相对比较简单,主要是调用函数pci_unregister_driver( )从Linux内核中注销设备驱动程序:

    static void __exit demo_cleanup_module (void)
    {
        pci_unregister_driver(&demo_pci_driver);
    }
    •  如何将驱动程序编译后加载进内核

    (1)编写Makefile文件

    makefile文件实例

    ifneq ($(KERNELRELEASE),)
    obj-m:=hello.o
    else
    #generate the path
    CURRENT_PATH:=$(shell pwd)
    #the absolute path
    LINUX_KERNEL_PATH:=/lib/modules/$(shell uname -r)/build
    #complie object
    default:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    clean:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
    endif

    obj-m 表示该文件要作为模块编译  obj-y则表示该文件要编译进内核

    正常情况下只需修改hello.o即可

    (2)执行make命令生成 *.ko 文件

    (3)sudo insmod *.ko加载驱动模块

    (4)sudo rmmod *.ko卸载驱动模块

    (5)使用dmesg | tail -10来查看内核输出的最后十条信息

    (6)使用modinfo *.ko来查看模块信息

    • PCI IO/内存地址区域

    一个PCI设备可实现多达6个I/O地址区域,每个区域既可以使内存也可以是I/O地址。在内核中PCI设备的I/O区域已经被集成到通用资源管理器。因此,我们无需访问配置变量来了解设备被映射到内存或者I/O空间的何处。获得区域信息的首选接口是下面的宏定义:

    #define pci_resource_start(dev, bar)((dev)->resource[(bar)].start)

     该宏返回六个PCI I/O区域之一的首地址(内存地址或者I/O端口号).该区域由整数的bar(base address register,基地址寄存器)指定,bar取值为0到5。

    #define pci_resource_end(dev, bar)((dev)->resource[(bar)].end)

     该宏返回第bar个I/O区域的首地址。注意这是最后一个可用的地址,而不是该区域之后的第一个地址。

    #define pci_resource_flags(dev, bar)((dev)->resource[(bar)].flags)

     该宏返回和该资源相关联的标志。资源标志用来定义单个资源的特性,对与PCI I/O区域相关的PCI资源,该信息从基地址寄存器中获得,但对于和PCI无关的资源,它可能来自其他地方。所有资源标志定义在<linux/ioport.h>。

    • PCIE驱动下的DMA及中断

    (1)DMA循环缓冲区的分配与实现:

           对于高速数据信号的采集处理,需要在驱动程序的初始化模块(probe)中申请大量的DMA循环缓冲区,申请的大小直接关系着能否实时对高速数据处理的成败。直接内存访问(DMA)是一种硬件机制,允许外围设备和主内存直接直接传输I/O数据,避免了大量的计算开销。

    (2) Linux内核的内存分区段:

           三个区段,可用于DMA的内存,常规内存以及高端内存。

    ·        通常的内存分配发生在常规内存区,但是通过设置内存标识也可以请求在其他区段中分配。可用于DMA的内存指存在于特别地址范围内的内存,外设可以利用这些内存执行DMA访问,进行数据通信传输。

    ·        DMA循环缓冲区的分配要求:物理连续,DMA可以访问,足够大。

    (3)Linux内存分配函数:

    ·        Linux系统使用虚拟地址,内存分配函数提供的都是虚拟地址,通过virt_to_bus转换才能得到物理地址。

    ·        分配内核内存空间的函数:kmalloc实现小于128KB的内核内存申请,申请空间物理连续;__get_free_pages实现最大4MB的内存申请,以页为单位,所申请空间物理连续;vmalloc分配的虚拟地址空间连续,但在物理上可能不连续。

    ·        Linux内核中专门提供了用于PCI设备申请内核内存的函数pci_alloc_consistent,支持按字节长度申请,该函数调用__get_free_pages,故一次最大为4MB。

    (4) DMA数据传输的方式:

    ·        一种是软件发起的数据请求(例如通过read函数调用),另一种是硬件异步将数据传给系统。对于数据采集设备,即便没有进程去读取数据,也要不断写入,随时等待进程调用,因此驱动程序应该维护一个环形缓冲区,当read调用时可以随时返回给用户空间需要的数据。

    (5)PCIe中向CPU发起中断请求的方式:

    ·        消息信号中断(MSI),INTx中断。

    ·        在MSI中断方式下,设备通过向OS预先分配的主存空间写入特定数据的方式请求CPU的中断服务,为PCIe系统首选的中断信号机制,对于PCIe到PCI/PCI-X的桥接设备和不能使用MSI机制的传统端点设备,采用INTx虚拟中断机制。

    ·        PCIe设备注册中断时使用共享中断方式,Linux系统通过request_irq实现中断处理程序的注册,调用位置在设备第一次打开,硬件产生中断之前;同样,free_irq时机在最后一次关闭设备,硬件不用中断处理器之后。

    ·        中断处理函数的功能是将有关中断接收的信息反馈给设备,并对数据进行相应读写。中断信号到来,系统调用相应的中断处理函数,函数判断中断号是否匹配,若是,则清除中断寄存器相应的位,即在驱动程序发起新的DMA之前设备不会产生其他中断,然后进行相应处理。

    (6)数据读写和ioctl控制:

    ·        数据读写:应用进程不需要数据时,驱动程序动态维护DMA环形缓冲区,当应用进程请求数据,驱动程序通过Linux内核提供copy_from_user()/copy_to_user()实现内核态和用户态之间的数据拷贝。

    ·        硬件控制:用户空间经常回去请求设备锁门,报告错误信息,设置寄存器等,这些操作都通过ioctl支持,可以对PCIe卡给定的寄存器空间进行配置。

    (7)中断处理程序的注册:

    ·        中断号在BIOS初始化阶段分配并写入设备配置空间,然后Linux在建立pci_dev时从配置空间中读出该中断号并写入pci_dev的irq成员中,所以注册中断程序时直接从pci_dev中读取就行。

    ·        当设备发生中断,8259A将中断号发给CPU,CPU根据中断号找到中断处理程序,执行。

    (8)DMA数据传输机制的产生:

    ·        传统经典过程:数据到达网卡 -> 网卡产生一个中断给内核 -> 内核使用 I/O 指令,从网卡I/O区域中去读取数据。这种方式,当大流量数据到来时,网卡会产生大量中断,内核在中断上下文中,会浪费大量资源处理中断本身。

    ·        改进:NAPI,即轮询,即内核屏蔽中断,隔一定时间去问网卡,是否有数据。则在数据量小的情况下,这种方式会浪费大量资源。

    ·        另一个问题,CPU到网卡的I/O区域,包括I/O寄存器和I/O内存中读取,再放到系统物理内存,都占用大量CPU资源,做改进,即有了DMA,让网卡直接从主内存之间读写自己的I/O数据。

    ·        首先,内核在主内存中为收发数据建立一个环形的缓冲队列(DMA环形缓冲区),内核将这个缓冲区通过DMA映射,将这个队列交给网卡;网卡收到数据,直接放进环形缓冲区,即直接放到主内存,然后向系统产生中断;

    ·        内核收到中断,取消DMA映射,可以直接从主内存中读取数据。

     

    转载于:https://my.oschina.net/u/3732258/blog/1612554

    展开全文
  • 基于linux 的 PCI & PCIe 总线分析总结

    千次阅读 2019-02-02 19:10:41
    PCIe 总线分析总结 前言 讲解PCI &amp; PCIe 的书有很多,我手上就拿了一本《PCI Express 体系结构导读》的书,据说这本书基本是翻译了外文,书上虽然内容比较全面,但是书那么厚,想达到快速掌握的目的还有...

    基于linux 的 PCI & PCIe 总线分析总结

    前言

    讲解PCI & PCIe 的书有很多,我手上就拿了一本《PCI Express 体系结构导读》的书,据说这本书基本是翻译了外文,书上虽然内容比较全面,但是书那么厚,想达到快速掌握的目的还有一定难度;网上也有很多相关博客,但是普遍很浅,内容局限又支离破碎;这就是写这篇总结的目的,从我的理解出发,对PCI & PCIe 做一个总结。

    内容聚焦于下面几个方面:

    • 从软件开发者角度来总结 PCI 基础知识,理清楚各种概念
    • 从软件开发者角度来总结 PCIe 的基础知识,在 PCI 的基础上,并且能够与 PCI 进行对比
    • 基于linux,梳理内核 PCIe 驱动框架,初始化流程
    • 如何基于linux内核设备驱动框架写一个自己的PCIe驱动

    从软件开发者角度认识 PCI 总线

    PCI总线信号定义

    在这里插入图片描述

    PCI总线拓扑图

    在这里插入图片描述

    PCI是并行总线,是总线型拓扑结构,图中给出了一个比较复杂的PCI总线拓扑,借此来说明PCI总线的组成,初始化等部分

    CPU域 & DRAM域 & 存储器域 & PCI总线域

    在描述PCI总线拓扑之前要先讲清楚图中的这几个域,其中有两个域是一定要区分清楚的,那就是存储器域与PCI总线域。域应该怎么理解呢?我理解为地址空间,PCI总线有其独立的地址空间,SOC也有其独立的地址空间,这两个地址空间不能搞混。

    • CPU域
    • DRAM域
    • PCI总线域 - PCI设备能够直接使用的地址是PCI总线域的地址,在PCI总线事务中出现的地址也是PCI总线域的地址
    • 存储器域 - 处理器能够直接使用的地址是存储器域的地址

    CPU所能访问的PCI总线地址一定在存储器域中有地址映射;
    PCI设备能够访问的存储器域的地址也一定在PCI总线域中具有地址映射。

    PCI总线拓扑组成

    PCI总线拓扑主要由三部分构成:

    • HOST主桥
    • PCI总线
    • PCI设备

    HOST主桥

    HOST主桥用来隔离处理器系统的存储器域与处理器系统的PCI总线域,管理PCI总线域,完成处理器与PCI设备间的数据交换。这也是我们为什么一上来就说要区分存储器域与PCI总线域了,因为HOST主桥的作用之一就是隔离这两个域。图中的拓扑带有两个HOST主桥,HOST主桥X与HOST主桥Y.

    PCI总线

    PCI总线由HOST主桥或者PCI桥管理,用来连接各类设备。图中给出了PCI总线0,1,2,3,4. HOST主桥出的总线编号号PCI总线0,在一棵PCI总线树中有多少个PCI桥(包含HOST主桥),就有多少条PCI总线。

    PCI设备

    在PCI总线中有三类设备:PCI主设备、PCI从设备、PCI桥设备。

    • PCI主设备 - 可以通过总线仲裁获得PCI总线使用权,主动向其他PCI设备或者主存储器发起读写请求;
    • PCI从设备 - 只能被动的接收来自HOST主桥或者其他PCI设备的读写请求;
    • PCI桥设备 - 主要是管理下游的PCI设备,并转发上下游之间的总线事务。PCI桥可以扩展PCI总线,但是不能扩展PCI总线域,比如当前系统使用的是32bit的PCI总线地址,那么这个系统的PCI总线域的地址空间就是4G,与桥的个数无关。

    一个PCI设备既可以是主设备也可以是从设备,但同一时刻只能有一种角色。

    PCI设备的配置空间

    上述的描述讲了PCI总线的拓扑,拓扑中每一个部分的功能。那么接下来就会有疑问:我们(CPU)是如何访问PCI设备的?
    知道了PCI设备与CPU如何交互,我们才能去写驱动代码,所以这个问题要搞清楚。

    CPU访问PCI设备的配置空间使用的是ID寻址方式;CPU访问PCI设备的存储器IO地址空间采用的是*地址寻址***方式

    一下子出现了这么多名词,配置空间、ID寻址、存储器地址空间、IO地址空间,一个个讲清楚就知道PCI到底是如何访问的了。

    猜测的为什么要有配置空间

    CPU访问PCI设备是通过地址映射进行的,PCI设备内部的地址空间(PCI总线域的一部分)会通过一定方式映射到SOC能够访问到的一段地址空间中(存储器域的一部分),借助DMA来实现通信。这个地址映射关系总要有个地方记录吧;还有插入一个PCI设备,总要知道它的一些基本信息吧,比如厂家信息,版本信息…;插入的PCI设备是哪一种得有能区分的点吧。一旦遇到这种问题,大多数解决方案都是规定一个统一的格式,大家都按照这个格式来填写,而且要提供一种通用的方式能够获取到这个格式的数据,这种通用性必须能够屏蔽PCI设备间的差异。于是乎就有了配置空间这么个玩意。

    PCI配置空间的分类

    PCI设备都有独立的配置空间,分为三种类型:

    • PCI Agent 使用的配置空间
    • PCI桥使用的配置空间
    • Cardbus桥片使用的配置空间

    PCI Agent配置空间和PCI桥配置空间需要重点了解,Cardbus这个我也不知道是什么鬼。

    PCI Agent 配置空间

    在这里插入图片描述

    • Device ID 和 Verdor ID. 厂商分配的,只读。
    • Revision ID. 记录了PCI设备的版本号,只读
    • Class Code. 记录了PCI设备的分类,只读。分为base class code(把设备分为显卡、网卡、PCI桥等设备)、sub class code(进行细分)、interface(定义编程接口) 三个字段。这个寄存器可供系统软件识别当前PCI设备的分类。
    • Header Type. 表示当前配置空间类型,只读。
    • Cache Line Size. HOST处理器使用的Cache行长度,由系统软件设置。(对PCIe无意义)
    • Subsystem ID 和 subSystem Vendor ID. 当使用Device ID 和 Vendor ID 无法区分的情况
    • Expansion ROM base address. 记录了ROM程序的基地址。有些PCI设备在处理器还没有运行操作系统之前,就需要完成基本的初始化设置,PCI设备提供了一段ROM程序,处理器在初始化过程中将运行这段ROM程序,初始化这些PCI设备。
    • Capabilities Pointer. 在PCI设备中,寄存器是可选的,在PCI-X和PCIe设备中必须支持。
    • Interrupt Line. 系统软件对PCI设备进行配置的时候写入的,记录当前PCI设备使用的中断向量号,如果不适用8259A中断控制器,这个寄存器没有意义
    • Interrupt Pin.
    • Base Address Register 0 ~ 5. 保存PCI设备使用的地址空间的基地址,保存的是该设备在PCI总线域中的地址。

    绝大多数PCI设备将PCI配置信息存放在E2PROM中,PCI设备进行上电初始化,将E2PROM中的信息读到PCI设备的配置空间中作为初始值,这个操作由硬件完成。

    PCI桥配置空间

    在这里插入图片描述

    PCI桥的配置空间在系统软件遍历PCI总线树时进行配置,系统软件不需要专门的驱动程序设置PCI桥的使用方法,PCI桥一般来讲是透明的。

    PCI桥有两组BAR寄存器,如果PCI桥本身不存在私有寄存器,那么BAR寄存器可以不使用(透明桥),初始化为0.

    PCI Bridge 的配置空间相比较 PCI Agent 的配置空间,多了 Bus Number 寄存器

    • Subordinate Bus Number 寄存器存放当前PCI子树中编号最大的PCI总线号
    • Secondary Bus Number 存放当前PCI桥Secondary Bus使用的总线号,也是该子树中编号最小的总线号
    • Primary Bus Number 存放该PCI桥上游的PCI总线号

    PCI总线树Bus号的初始化

    PCI桥的配置空间中有三个 Bus Number 寄存器,这三个寄存器是需要软件在初始化PCI总线的时候填写的,也就是PCI总线树Bus号的初始化。

    系统软件在遍历当前PCI总线树时,需要首先对这些PCI总线进行编号,即初始化PCI桥的Primary、Secondary、Subordinate Bus Number 寄存器。编号时使用深度优先算法。再次强调,在一棵PCI总线树中有多少个PCI桥(包含HOST主桥),就有多少条PCI总线。

    在PCI总线拓扑图中,HOST主桥X直接出的PCI总线就是PCI Bus 0
    
    HOST主桥扫描(这个扫描具体是怎样一种动作?)PCI总线0上的设备,系统软件首先忽略这条总线上的所有PCI Agent,
    HOST主桥就发现了PCI桥,命名为X1, 把这个桥出的总线定为PCI Bus 1, 将PCI桥X1的Primary Bus Number 赋为0,
    因为这个桥是接在PCI总线0上的,把Secondary Bus Number 寄存器赋值为1,因为它引出的PCI总线为Bus 1;
    
    继续扫描PCI Bus 1,又发现了一个桥,命名为PCI桥X2, 把这个桥出的总线定为PCI Bus 2, 将PCI桥X2的Primary Bus Number为1,Secondary Bus Number为2;
    
    继续扫描PCI Bus 2,发现了新桥,命名为PCI桥X3,将这个桥出的总线定为PCI Bus 3, 将PCI桥X3的Primary Bus Number设置为2,Secondary Bus Number设置为3;
    
    继续扫描PCI Bus 3,没有发现PCI桥,也就是说PCI总线3下面不会有新的总线了,就把PCI桥X3的Subordinate Bus Number 赋值为3,并且回退到PCI Bus 2;
    
    继续扫描PCI Bus 2,没有发现除PCI桥X3之外的桥,把PCI桥X2的Subordinate Bus Number 也赋值为3,并回退到PCI Bus 1;
    
    继续扫描PCI Bus 1, 没有发现除PCI桥X2之外的桥,把PCI桥X1的Subordinate Bus Number 也赋值为3,并回退到PCI Bus 0;
    
    继续扫描PCI Bus 0, 发现了新桥,命名为PCI桥X4,将PCI桥X4的出的总线定为PCI Bus 4, 将PCI桥X4的Primary Bus Number设置为0,Secondary Bus Number设置为4;
    
    继续扫描PCI Bus 4,没有发现新桥,把PCI桥X4的Subordinate Bus Number 赋值为4, 并回退到PCI Bus 0;
    
    继续扫描PCI Bus 0, 没有发现新桥,结束遍历,完成Bus号分配。
    

    PCI设备配置空间的访问机制

    ID寻址

    HOST主桥通过配置读写总线事务(这是总线事务的一种,什么是总线事务?这…)访问配置空间。

    配置读写总线事务通过ID号进行寻址。ID号由总线号、设备号、功能号组成。

    • 总线号:在一棵PCI总线树中,有几个PCI桥就有几条PCI总线(包含HOST主桥);总线号由系统软件决定。系统软件使用DFS(深度优先)算法扫描PCI总线树上的所有PCI总线,并进行编号。
    • 设备号:由PCI设备的IDSEL信号与PCI总线地址线的连接关系确定。
    • 功能号:与PCI设备的具体设计相关,一个PCI设备中最多有8个功能号,每个功能设备有自己的配置空间

    HOST主桥使用寄存器号来访问PCI设备配置空间的某个寄存器。

    配置请求

    PCI总线有两类配置请求:

    • Type 00h 配置请求. 访问与HOST主桥或者PCI桥直接相连的PCI Agent或者PCI桥
    • Type 01h 配置请求. 访问没有直接相连的PCI Agent 或者PCI桥

    以x86处理器来讲,PCI控制器提供了CONFIG_ADDRESS寄存器和CONFIG_DATA寄存器,就是通过这两个寄存器来控制配置读写总线事务

    CONFIG_ADDRESS寄存器与Type 01h配置请求的对应关系

    在这里插入图片描述

    CONFIG_ADDRESS寄存器与Type 00h配置请求的对应关系

    在这里插入图片描述

    在PCI总线中,只有PCI桥能够接收Type 01h 配置请求,Type 01h 配置请求不能直接发向最终的PCI Agent设备,而只能由PCI桥将其转换为Type 01h 请求继续发向其他PCI桥或者转换为Type 00h 配置请求发向PCI Agent 设备

    在PCI总线拓扑中,加入要访问PCI设备01

    HOST处理器访问PCI01的配置空间,发现PCI设备01与HOST主桥直接相连,
    所以将直接使用Type 00h 配置请求访问该设备的配置空间
    
    将CONFIG_ADDRESS 寄存器的Enabled位置1,
    Bus Number号置为0,并对该寄存器的Device, Function, Register Number字段赋值
    
    当HOST处理器对CONFIG_DATA寄存器访问时,
    HOST主桥将存放在CONFIG_ADDRESS寄存器中的数值转换为Type 00h配置请求,
    并发送到PCI总线0
    
    PCI01设备接收到这个Type 00h配置请求,进行交互。
    

    在PCI总线拓扑中,假如要访问PCI设备31

    HOST处理器访问PCI设备31的配置空间,需要通过HOST主桥、PCI桥X1、X2和X3,最到达PCI31。
    
    首先将CONFIG_ADDRESS的 Bus Number 置为3;
    
    然后当HOST处理器对CONFIG_DATA寄存器进行读写访问时,HOST主桥将Type 01h 的配置请求发送到PCI总线0;
    
    PCI Bus 0上的PCI桥X1接收配置请求。
    PCI桥X1的Secondary Bus Number为1,Subordinate Bus Number为3, 1 < Bus Number <=3,
    所以PCI桥X1接收来自PCI总线0的Type 01h配置请求,并将这个配置请求发送到PCI Bus 1;
    
    PCI Bus 1上的PCI桥X2接收配置请求。
    PCI桥X2的Secondary Bus Number为2,Subordinate Bus Number为3, 2 < Bus Number <= 3,
    所以PCI桥X2接收来自PCI总线1的Type 01h配置请求, 并将这个配置请求发送到PCI Bus 2;
    
    PCI Bus 2上的PCI桥X3接收配置请求。
    PCI桥X3的Secondary Bus Number为3,Subordinate Bus Number为3,则要访问的设备就在这个桥下,
    PCI桥X3将Type 01h的总线事务转换成Type 00h的总线事务,发送到PCI总线3, PCI31 接收到了请求,进行交互。
    

    Device号的分配

    PCI设备的IDSEL信号与PCI总线的AD[31:0]信号的连接关系决定了该设备在这条PCI总线的设备号。
    每一个PCI设备都使用独立的IDSEL信号,其中CONFIG_ADDRESS寄存器中的Device Number 字段共有5位,可以表示32个设备,而AD[31:11]只有21位,这意味着一条PCI总线上最多能接21个设备。

    PCI 总线数据交换

    存储器域与PCI总线域的映射

    在这里插入图片描述

    PCI Agent 设备之间以及HOST 处理器和PCI Agent设备之间可以使用存储器读写和IO读写等总线事务进行数据传送送。大多数情况下, PCI桥不之间与PCI设备或者HOST主桥进行数据交换,仅仅是转发来自PCI Agent 或者 HOST 主桥的数据。

    在PCI Agent 设备进行数据传送之前,系统软件需要初始化PCI Agent 设备的 BAR0 ~ BAR5寄存器,以及PCI桥的Base, Limit寄存器,系统软件使用DFS算法对PCI总线进行遍历时,完成这些寄存器的初始化,分配这些设备在PCI总线域的地址空间,然后PCI设备就可以使用PCI总线地址进行数据传输了。

    PCI Agent 的BAR0 ~ 5, 以及 PCI bridge 的 Base 寄存器都是存的PCI总线地址,这些地址在处理器的存储器域有对应的映像,如果一个PCI设备的BAR空间在处理器的存储器域没有映像,处理器将不能访问PCI设备的BAR空间。

    处理器访问PCI设备地址空间

    HOST主桥隔离了PCI总线域与存储器域。在PCI总线初始化时,会在CPU存储器域中建立PCI设备的存储器地址空间的映射,当处理器访问设备的地址空间时,首先访问该设备在存储器域中的地址空间,并且通过HOST主桥的地址空间转换为PCI总线域的地址空间,再使用PCI总线事务将数据发送到指定的PCI设备中

    PCI设备访问存储器域的地址空间

    PCI设备访问存储器域的地址空间时通过DMA完成的。处理器需要将存储器域的地址空间反向映射到PCI总线地址空间。首先访问该储存器地址空间对应的PCI总线地址空间,通过HOST主桥将这个地址空间转换为存储器地址空间,再由DDR控制器对存储器进行读写访问。

    X86处理器的HOST主桥中,存储器域的存储器地址与PCI总线域的地址相等。

    PCI设备BAR寄存器和PCI桥 Base、Limit寄存器的初始化

    存储器空间与IO空间???

    PCI桥配置空间的图中,有一些Limit 和 Base 的寄存器,这些寄存器的作用就是记录该PCI桥所管理的PCI子树的存储器地址空间或者I/O地址空间的基地址和长度。

    • I/O Limit
    • I/O Base
    • Memory Limit
    • Memory Base

    现在假设如下:

    • CPU的存储器域的 0xF000-0000 ~ 0xF800-0000 与 PCI总线域的0x7000-0000 ~ 0x7800-0000 是映射关系 实际情况呢? PCI总线域的这个起始地址是如何确定的?
    • PCI Agent 设备只使用BAR0 寄存器 实际情况呢?一般都用几个?
    • PCI Agent 的存储器空间大小为16MB(0x100-0000) 实际情况呢?这个空间大小是如何确定的呢?

    BAR寄存器初始化和PCI总线的Bus号分配是在同一个动作中完成的

    软件遍历到PCI桥X3后,没有再探测到PCI Bus 3下面有PCI桥,这时候就为PCI Bus 3 下面的PCI Agent的BAR寄存器赋值(当然就是从0x7000-0000 ~ 0x7800-0000中分配)
    PCI-Agent31.BAR0 = 0x7000-0000; PCI-Agent32.BAR0 = 0x7100-0000;
    为PCI桥X3的Base, Limit 寄存器赋值 PCI-BridgeX3.Memory-Base = 0x7000-0000; PCI-BridgeX3.Memory-Limit = 0x200-0000;
    
    回退到PCI Bus 2, 发现PCI设备21, PCI-Agent21.BAR0 = 0x7200-0000;
    为PCI桥X2的Base, Limit 寄存器赋值 PCI-BridgeX2.Memory-Base = 0x7000-0000; PCI-BridgeX3.Memory-Limit = 0x300-0000;
    
    回退到PCI Bus 1, 发现PCI设备11, PCI-Agent11.BAR0 = 0x7300-0000;
    为PCI桥X1的Base, Limit 寄存器赋值 PCI-BridgeX1.Memory-Base = 0x7000-0000; PCI-BridgeX3.Memory-Limit = 0x400-0000;
    
    回退到PCI Bus 0, 发现PCI桥X4, 进入PCI Bus 4, 没有再探测到PCI Bus 4 下面有PCI桥,这时候就为PCI Bus 4 下面的PCI Agent的BAR寄存器赋值
    PCI-Agent41.BAR0 = 0x7400-0000; PCI-Agent42.BAR0 = 0x7500-0000;
    为PCI桥X4的Base, Limit 寄存器赋值 PCI-BridgeX4.Memory-Base = 0x7400-0000; PCI-BridgeX4.Memory-Limit = 0x200-0000;
    
    回退到PCI Bus 0, 没有再探测到PCI Bus 0下面有PCI桥, 这时候就为PCI Bus 4 下面的PCI Agent的BAR寄存器赋值
    PCI-Agent01.BAR0 = 0x7600-0000;
    
    遍历结束
    

    地址译码

    当一个存储器读写总线事务到达PCI总线时,在这条总线上所以的设备将进行地址译码,如果当前总线事务使用的地址在某个PCI设备的BAR空间中,该PCI设备将使能DVESEL#信号,认领总线事务。

    Posted 传送方式与Non-Posted 传送方式

    • Posted传送方式,数据通过PCI桥后将结束上一级总线的PCI事务,从而上一级PCI总线可以被其他PCI设备使用。
    • Non-Posted 传送方式,当数据达到目标设备后,目标设备需要向主设备发出回应,当主设备收到这个回应后才能结束整个总线事务。
    • 处理器向PCI设备进行读操作使用的是Non-Posted方式,写操作使用的是Posted方式
    • PCI设备的DMA写使用Posted方式,DMA读使用Non-Posted方式

    PCI设备读写主存储器

    PCI设备与存储器直接进行数据交换的过程是通过DMA实现的。支持DMA传递的PCI设备可以在BAR空间中设置两个寄存器,分别保存目标地址和传送大小。
    PCI设备进行DMA操作时,使用的目的地址是PCI总线域的物理地址,而不是存储器域的

    中断机制

    PCI提供了INTA#,INTB#,INTC#,INTD#信号向处理器发出中断请求,同时也提供了MSI机制向处理器提交中断请求

    MSI中断机制

    MSI中断机制采用存储器写总线事务向处理器系统提交中断请求,其实现机制是向HOST处理器指定的一个存储器地址写指定的数据。这个存储器地址一般是中断控制器规定的某段存储器地址范围,而且数据也是事先安排好的数据,通常含有中断向量号。
    MSI在PCIe上已经成为了主流,PCIe设备必须支持MSI中断机制,PCI设备不一定都支持,而且在PCI设备上不常用。

    从软件角度认识PCIe总线

    PCI总线是并行总线,同一条总线上,所有外部设备共享总线带宽,PCIe总线使用高速差分总线,并采用端到端的连接方式,在一条PCIe链路的两端只能各连接一个设备,这两个设备互为数据发送端和数据接收端。PCIe总线在设计过程中使用了一些网络通信的技术。(现在感觉高速数据有很多类似的地方,基本都涉及serdies)

    展开全文
  • Xilinx PCIe DMA Linux驱动代码分析

    千次阅读 2018-11-02 14:00:24
  • 我现在设了一个结构体指针数组用来放每次不同设备申请的地址,但是设备节点出不来,出来的话需要再open那里打开设备数量的const_of,但是open函数参数不是应用层打开节点传...我不确定是否思路有问题,能否帮忙分析分析
  • 要弄清楚Linux PCI设备驱动,首先要明白,所谓的Linux PCI设备驱动实际包括Linux PCI设备驱动和设备本身驱动两部分。不知道读者理不理解这句话,本人觉得这句话很重要,对于PCI、USB这样的驱动来说,必须要理解这个...
  • PCIe实践之路:Linux访问PCIe空间

    万次阅读 2017-09-03 20:32:16
    Linux在枚举PCIe设备的过程由内核中的PCI框架负责,在EP配置完成之后,驱动通过以下接口访问PCIe空间,原理参考前文《大话PCIe:设备枚举》一、访问配置空间相关接口位于drivers/pci/access.c1.1 读配置空间 ...
  • Linux平台PCIe驱动编写

    千次阅读 2019-03-21 10:27:58
    以前文章分析PCIe整个系统知识,包括如何扫描PCIE树,这篇文章讲解一下当拿到一个PCIe设备时如何编写驱动程序。编写驱动程序在应用程序中编写,同样可以在内核层编写驱动。 从应用层编写驱动主要是使用pcilib库和...
  • 前言 本文基于Linux kernel 4.19.0, 体系结构为aarch64. PCIe hotplug
  • linuxPCIE控制器设备树 学习

    千次阅读 2017-07-12 20:15:29
    本系列全部内容基于Synopsys DesignWare进行学习与分析 在内核3.x之后开始使用设备树,所有和硬件相关部分均放在设备树中,在进行PCIE控制器学习时,对官方文档中各个变量的使用存在很大的迷惑,根据自己了解...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,800
精华内容 1,120
关键字:

linuxpcie分析

linux 订阅