1. 程式人生 > >嵌入式Linux裝置驅動開發(二)

嵌入式Linux裝置驅動開發(二)

上一篇中介紹到裝置驅動如何匹配裝置以及繫結裝置的,在Linux系統下進行註冊,這裡將繼續介紹probe函式的功能。
5、probe函式
Probe()函式必須驗證指定裝置的硬體是否真的存在,probe()可以使用裝置的資源,包括時鐘,platform_data等。一般來說裝置是不能被熱插拔的,所以可以將probe()函式放在init段裡面來節省driver執行時候的記憶體開銷。

probe函式在裝置驅動註冊最後收尾工作,當裝置的device 和其對應的driver 在總線上完成配對之後,系統就呼叫platform裝置的probe函式完成驅動註冊最後工作。資源、中斷呼叫函式以及其他相關工作。

probe函式接收到plarform_device這個引數後,就需要從中提取出需要的資訊。它一般會通過呼叫核心提供的platform_get_resource和platform_get_irq等函式來獲得相關資訊。如通過platform_get_resource獲得裝置的起始地址後,可以對其進行request_mem_region和ioremap等操作,以便應用程式對其進行操作。通過platform_get_irq得到裝置的中斷號以後,就可以呼叫request_irq函式來向系統申請中斷。這些操作在裝置驅動程式中一般都要完成。
在完成了上面這些工作和一些其他必須的初始化操作後,就可以向系統註冊我們在/dev目錄下能看在的裝置檔案了。

/**
 * irfpa_drv_probe -  Probe call for the device.
 //*針對裝置探測驅動
 * @pdev:   handle to the platform device structure.
 //*引數是平臺裝置的結構體。分配儲存區,註冊裝置
 * It does all the memory allocation and registration for the device.//0成功,其他負數
 * Returns 0 on success, negative error otherwise.
 **/
 static int __devinit irfpa_drv_probe(struct platform_device *
pdev) { struct resource *irfpa_regs_res; struct resource *vdma_regs_res; struct resource *vbuf_mem_res; struct resource *xinfo_mem_res; struct irfpa_drvdata *drvdata; dev_t devt; int retval; //參考裝置樹檔案中對裝置包括的幾個地址區間 irfpa_regs_res = platform_get_resource(pdev, IORESOURCE_MEM, 0); vdma_regs_res = platform_get_resource(pdev, IORESOURCE_MEM, 1); vbuf_mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 2); xinfo_mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 3); if ((!irfpa_regs_res) || (!vdma_regs_res) || (!vbuf_mem_res) || (!xinfo_mem_res)) { dev_err(&pdev->dev, "Invalid address.\n"); return -ENODEV; } /*組合裝置號符合dev_t型別 devt = MKDEV(IRFPA_MAJOR, IRFPA_MINOR); retval = register_chrdev_region(devt, IRFPA_DEVICES, DRIVER_NAME); if (retval < 0) return retval; */ retval = alloc_chrdev_region(&devt, IRFPA_MINOR, IRFPA_DEVICES, DRIVER_NAME);//動態分配裝置編號,該函式需要傳遞給它指定的第一個次裝置號firstminor(一般為0)和要分配的裝置數count,以及裝置名,呼叫該函式後自動分配得到的裝置號儲存在dev中。 if (retval < 0) { dev_err(&pdev->dev, "alloc_chrdev_region fail.\n"); return retval; } drvdata = kzalloc(sizeof(struct irfpa_drvdata), GFP_KERNEL);//用kzalloc申請記憶體的時候, 效果等同於先是用 kmalloc() 申請空間 , 然後用 memset() 來初始化 ,所有申請的元素都被初始化為 0.GFP_KERNEL 核心記憶體的正常分配. 可能睡眠. if (!drvdata) { dev_err(&pdev->dev, "Couldn't allocate device private record.\n"); retval = -ENOMEM; goto failed0; } dev_set_drvdata(&pdev->dev, (void *)drvdata); drvdata->devt = devt; drvdata->is_open = 0; mutex_init(&drvdata->sem); // == request_mem_region if (!request_mem_region(irfpa_regs_res->start, irfpa_regs_res->end - irfpa_regs_res->start + 1, DRIVER_NAME)) { dev_err(&pdev->dev, "Couldn't lock memory region at %Lx\n", (unsigned long long) irfpa_regs_res->start); retval = -EBUSY; goto failed1_1; } if (!request_mem_region(vdma_regs_res->start, vdma_regs_res->end - vdma_regs_res->start + 1, DRIVER_NAME)) { dev_err(&pdev->dev, "Couldn't lock memory region at %Lx\n", (unsigned long long) vdma_regs_res->start); retval = -EBUSY; goto failed1_2; } if (!request_mem_region(vbuf_mem_res->start, vbuf_mem_res->end - vbuf_mem_res->start + 1, DRIVER_NAME)) { dev_err(&pdev->dev, "Couldn't lock memory region at %Lx\n", (unsigned long long) vbuf_mem_res->start); retval = -EBUSY; goto failed1_3; } if (!request_mem_region(xinfo_mem_res->start, xinfo_mem_res->end - xinfo_mem_res->start + 1, DRIVER_NAME)) { dev_err(&pdev->dev, "Couldn't lock memory region at %Lx\n", (unsigned long long) xinfo_mem_res->start); retval = -EBUSY; goto failed1_4; } // ==== ioremap ==== drvdata->irfpa_base_address = ioremap(irfpa_regs_res->start, (irfpa_regs_res->end - irfpa_regs_res->start + 1)); if (!drvdata->irfpa_base_address) { dev_err(&pdev->dev, "irfpa_reg_res ioremap() failed\n"); goto failed2_1; } drvdata->vdma_base_address = ioremap(vdma_regs_res->start, (vdma_regs_res->end - vdma_regs_res->start + 1)); if (!drvdata->vdma_base_address) { dev_err(&pdev->dev, "vdma_reg_res ioremap() failed\n"); goto failed2_2; } drvdata->vbuf_base_address = ioremap(vbuf_mem_res->start, (vbuf_mem_res->end - vbuf_mem_res->start + 1)); if (!drvdata->vbuf_base_address) { dev_err(&pdev->dev, "vbuf_mem_res ioremap() failed\n"); goto failed2_3; } drvdata->vbuf_current_address = drvdata->vbuf_base_address; drvdata->xinfo_base_address = ioremap(xinfo_mem_res->start, (xinfo_mem_res->end - xinfo_mem_res->start + 1)); if (!drvdata->xinfo_base_address) { dev_err(&pdev->dev, "xinfo_mem_res ioremap() failed\n"); goto failed2_4; } // ==== print ioremap info === dev_info(&pdev->dev, "ioremap %llx to %p with size %llx\n", (unsigned long long) irfpa_regs_res->start, drvdata->irfpa_base_address, (unsigned long long) (irfpa_regs_res->end - irfpa_regs_res->start + 1)); dev_info(&pdev->dev, "ioremap %llx to %p with size %llx\n", (unsigned long long) vdma_regs_res->start, drvdata->vdma_base_address, (unsigned long long) (vdma_regs_res->end - vdma_regs_res->start + 1)); dev_info(&pdev->dev, "ioremap %llx to %p with size %llx\n", (unsigned long long) vbuf_mem_res->start, drvdata->vbuf_base_address, (unsigned long long) (vbuf_mem_res->end - vbuf_mem_res->start + 1)); dev_info(&pdev->dev, "ioremap %llx to %p with size %llx\n", (unsigned long long) xinfo_mem_res->start, drvdata->xinfo_base_address, (unsigned long long) (xinfo_mem_res->end - xinfo_mem_res->start + 1)); cdev_init(&drvdata->cdev, &irfpa_fops); drvdata->cdev.owner = THIS_MODULE; drvdata->cdev.ops = &irfpa_fops; retval = cdev_add(&drvdata->cdev, devt, 1); if (retval) { dev_err(&pdev->dev, "cdev_add() failed\n"); goto failed3; } /* create sysfs files for the device */ retval = sysfs_create_group(&(pdev->dev.kobj), &irfpa_attr_group); if (retval) { dev_err(&pdev->dev, "Failed to create sysfs attr group\n"); goto failed4; } irfpa_device_init(drvdata->irfpa_base_address, drvdata->vdma_base_address, vbuf_mem_res->start); drvdata->irfpa_xinfo_pinpon = irfpa_readreg(drvdata->irfpa_base_address + IRFPA_XINFO_PINPON); drvdata->nuc_buffer_pinpon = 1; return 0; /* Success */ failed4: cdev_del(&drvdata->cdev); failed3: iounmap(drvdata->xinfo_base_address); failed2_4: iounmap(drvdata->vbuf_base_address); failed2_3: iounmap(drvdata->vdma_base_address); failed2_2: iounmap(drvdata->irfpa_base_address); failed2_1: release_mem_region(xinfo_mem_res->start, xinfo_mem_res->end - xinfo_mem_res->start + 1); failed1_4: release_mem_region(vbuf_mem_res->start, vbuf_mem_res->end - vbuf_mem_res->start + 1); failed1_3: release_mem_region(vdma_regs_res->start, vdma_regs_res->end - vdma_regs_res->start + 1); failed1_2: release_mem_region(irfpa_regs_res->start, irfpa_regs_res->end - irfpa_regs_res->start + 1); failed1_1: kfree(drvdata); failed0: unregister_chrdev_region(devt, IRFPA_DEVICES); return retval; }

a、struct resource *irfpa_regs_res;

struct resource {
                const char *name;
                unsigned long start, end;
                unsigned long flags;
                struct resource *parent, *sibling, *child;
             };

linux採用struct resource結構體來描述一個掛接在cpu總線上的裝置實體(32位cpu的匯流排地址範圍是0~4G):
resource->start描述裝置實體在cpu總線上的線性起始實體地址;
resource->end -描述裝置實體在cpu總線上的線性結尾實體地址;
resource->name 描述這個裝置實體的名稱,這個名字開發人員可以隨意起,但最好貼切;
resource->flag 描述這個裝置實體的一些共性和特性的標誌位;
只需要瞭解一個裝置實體的以上4項,linux就能夠知曉這個掛接在cpu匯流排的上的裝置實體的基本使用情況,也就是[resource->start, resource->end]這段實體地址現在是空閒著呢,還是被什麼裝置佔用著呢?
linux會堅決避免將一個已經被一個裝置實體使用的匯流排實體地址區間段[resource->start, resource->end],再分配給另一個後來的也需要這個區間段或者區間段內部分地址的裝置實體,進而避免裝置之間出現對同一匯流排實體地址段的重複引用,而造成對唯一實體地址的裝置實體二義性.

以上的4個屬性僅僅用來描述一個裝置實體自身,或者是裝置實體可以用來自治的單元,但是這不是linux所想的,linux需要管理4G物理匯流排的所有空間,所以掛接到總線上的形形色色的各種裝置實體,這就需要鏈在一起,因此resource結構體提供了另外3個成員:指標parent、sibling和child:分別為指向父親、兄弟和子資源的指標。      
以root source為例,root->child(*pchild)指向root所有孩子中地址空間最小的一個;pchild->sibling是兄弟連結串列的開頭,指向比自己地址空間大的兄弟。

實體記憶體頁面是重要的資源。從另一個角度看,地址空間本身,或者物理儲存器在地址空間中的位置,也是一種資源,也要加以管理 -- resource管理地址空間資源。

核心中有兩棵resource樹,一棵是iomem_resource, 另一棵是ioport_resource,分別代表著兩類不同性質的地址資源。兩棵樹的根也都是resource資料結構,不過這兩個資料結構描述的並不是用於具體操作物件的地址資源,而是概念上的整個地址空間。
將主機板上的ROM空間納入iomem_resource樹中;系統固有的I/O類資源則納入ioport_resource樹  

b、irfpa_regs_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

struct resource *platform_get_resource(struct platform_device *dev,
                                   unsigned int type, unsigned int num)
{
       int i;

       for (i = 0; i < dev->num_resources; i++) {
              struct resource *r = &dev->resource[i];//不管你是想獲取哪一份資源都從第一份資源開始搜尋。

              if (type == resource_type(r) && num-- == 0)//首先通過type == resource_type(r)判斷當前這份資源的型別是否匹配,如果匹配則再通過num-- == 0判斷是否是你要的,如果不匹配重新提取下一份資源而不會執行num-- == 0這一句程式碼。
                     return r;
       }
       return NULL;
}

unsigned int type決定資源的型別,unsigned int num決定type型別的第幾份資源(從0開始)。即使同類型資源在資源陣列中不是連續排放也可以定位得到該資源。
函式進入for裡面,i=0,num_resources=7,拿出resource[0]資源。resource_type(r)提取出該份資源 的資源型別並與函式傳遞下來的資源型別進行比較,匹配。Num=0(這裡先判斷是否等於0再自減1)符合要求,從而返回該資源。

c、retval = alloc_chrdev_region(&devt, IRFPA_MINOR, IRFPA_DEVICES, DRIVER_NAME);//動態分配裝置編號,該函式需要傳遞給它指定的第一個次裝置號firstminor(一般為0)和要分配的裝置數count,以及裝置名,呼叫該函式後自動分配得到的裝置號儲存在dev中。

d、drvdata = kzalloc(sizeof(struct irfpa_drvdata), GFP_KERNEL);//用kzalloc申請記憶體的時候, 效果等同於先是用 kmalloc() 申請空間 , 然後用 memset() 來初始化 ,所有申請的元素都被初始化為 0.GFP_KERNEL 核心記憶體的正常分配. 可能睡眠.

e、dev_set_drvdata(&pdev->dev, (void *)drvdata);
將ndev儲存成平臺匯流排裝置的私有資料,提取用dev_get_drvdata

int dev_set_drvdata(struct device *dev, void *data)  
{  
         int error;  

         if (!dev->p) {  
                 error = device_private_init(dev);  
                 if (error)  
                         return error;  
         }  
         dev->p->driver_data = data;  
         return 0;  
 }

從程式碼中,我們可以看到此函式主要是把drvdata賦給了device->p->driver_data指標。那麼,我們下面來看一下,Kernel中比較重要的Device結構體,它其實是對核心中所有裝置的抽象表示。 所有的裝置都有一個device例項與之對應,而且Device結構體的主要用法為將其嵌入到其他的裝置結構體中,如platform_device等。同時,Device結構體也負責作為子系統之間互動的統一引數。

device結構體的構成。

include/linux/device.h
struct device {
     struct device *parent;
     struct device_private *p;  //負責儲存driver核心部分的資料
     struct kobject kobj;
     const char *init_name;
     .......
     struct device_driver *driver;
#ifdef CONFIG_PINCTRL
     struct dev_pin_info *pins;
#endfi
     .......
     struct device_node *of_node;  //負責儲存device_tree中相應的node地址
     .......
     const struct attribute_group **groups;
     ......
}

struct device_private {
     struct klist klist_children;
     struct klist_node knode_parent;
     struct klist_node knode_driver;
     struct klist_node knode_bus;
     struct list_head deferred_probe;
     void *driver_data;        //負責儲存driver中相應的driver_data
     struct device *device;
}

那麼,driver_data是何時進行初始化的呢?我們通過追蹤程式碼是可以發現,一般driver_data的初始化是發生在Driver檔案中的probe函式中的。

在probe函式中,malloc完相應的driver data結構體,填充完相應的域後,就會將driver data的地址賦值給driver data。這樣,在實現與其他子系統互動的介面時,就能通過其他子系統傳遞過來的device指標來找到相應的driver data。

f、mutex_init(&drvdata->sem);
初始化這個sem為互斥鎖

g、request_mem_region(irfpa_regs_res->start, irfpa_regs_res->end - irfpa_regs_res->start + 1, DRIVER_NAME)) (irfpa_regs_res->start, irfpa_regs_res->end - irfpa_regs_res->start + 1, DRIVER_NAME))

#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name))
//__request_region檢查是否可以安全佔用起始實體地址S1D_PHYSICAL_REG_ADDR之後的連續S1D_PHYSICAL_REG_SIZE位元組大小空間

一般來說,在系統執行時,外設的I/O記憶體資源的實體地址是已知的,由硬體的設計決定。但是CPU通常並沒有為這些已知的外設I/O記憶體資源的實體地址預定義虛擬地址範圍,驅動程式並不能直接通過實體地址訪問I/O記憶體資源,而必須將它們對映到核心虛地址空間內(通過頁表),然後才能根據對映所得到的核心虛地址範圍,通過訪內指令訪問這些I/O記憶體資源。Linux在io.h標頭檔案中聲明瞭函式ioremap(),用來將I/O記憶體資源的實體地址對映到核心虛地址空間。
但要使用I/O記憶體首先要申請,然後才能對映,使用I/O埠首先要申請,或者叫請求,對於I/O埠的請求意思是讓核心知道你要訪問這個埠,這樣核心知道了以後它就不會再讓別人也訪問這個埠了.畢竟這個世界僧多粥少啊.申請I/O埠的函式是request_region, 申請I/O記憶體的函式是request_mem_region。request_mem_region函式並沒有做實際性的對映工作,只是告訴核心要使用一塊記憶體地址,宣告佔有,也方便核心管理這些資源。

h、drvdata->irfpa_base_address = ioremap(irfpa_regs_res->start, (irfpa_regs_res->end - irfpa_regs_res->start + 1));
ioremap主要是檢查傳入地址的合法性,建立頁表(包括訪問許可權),完成實體地址到虛擬地址的轉換。如果出錯,iounmap函式用於取消ioremap()所做的對映。

void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
void *ioremap(unsigned long phys_addr, unsigned long size)
入口: phys_addr:要對映的起始的IO地址;
size:要對映的空間的大小;
flags:要對映的IO空間的和許可權有關的標誌;
phys_addr:是要對映的實體地址
size:是要對映的長度,單位是位元組
標頭檔案:io.h

功能:將一個IO地址空間對映到核心的虛擬地址空間上去,便於訪問;
實現:對要對映的IO地址空間進行判斷,低PCI/ISA地址不需要重新對映,也不允許使用者將IO地址空間對映到正在使用的RAM中,最後申請一個 vm_area_struct結構,呼叫remap_area_pages填寫頁表,若填寫過程不成功則釋放申請的vm_area_struct空間;
ioremap 依靠 __ioremap實現,它只是在__ioremap中以第三個引數為0呼叫來實現.
ioremap是核心提供的用來對映外設暫存器到主存的函式,我們要對映的地址已經從pci_dev中讀了出來(上一步),這樣就水到渠成的成功映射了而不會和其他地址有衝突。對映完了有什麼效果呢,我舉個例子,比如某個網絡卡有100 個暫存器,他們都是連在一塊的,位置是固定的,假如每個暫存器佔4個位元組,那麼一共400個位元組的空間被對映到記憶體成功後,ioaddr就是這段地址的開頭(注意ioaddr是虛擬地址,而mmio_start是實體地址,它是BIOS得到的,肯定是實體地址,而保護模式下CPU不認實體地址,只認虛擬地址),ioaddr+0就是第一個暫存器的地址,ioaddr+4就是第二個暫存器地址(每個暫存器佔4個位元組),以此類推,我們就能夠在記憶體中訪問到所有的暫存器進而操控他們了。

在將I/O記憶體資源的實體地址對映成核心虛地址後,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O記憶體資源了。為了保證驅動程式的跨平臺的可移植性,我們應該使用Linux中特定的函式來訪問I/O記憶體資源,而不應該通過指向核心虛地址的指標來訪問。

i、cdev_init(&drvdata->cdev, &irfpa_fops);

 struct cdev {
   struct kobject kobj;          // 每個 cdev 都是一個 kobject
   struct module *owner;       // 指向實現驅動的模組
   const struct file_operations *ops;   // 操縱這個字元裝置檔案的方法
   struct list_head list;       // 與 cdev 對應的字元裝置檔案的 inode->i_devices 的連結串列頭
   dev_t dev;                   // 起始裝置編號
   unsigned int count;       // 裝置範圍號大小
};
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
   memset(cdev, 0, sizeof *cdev);
   INIT_LIST_HEAD(&cdev->list);
   kobject_init(&cdev->kobj, &ktype_cdev_default);
   cdev->ops = fops;
}

一個 cdev 一般它有兩種定義初始化方式:靜態的和動態的。

靜態記憶體定義初始化:
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
動態記憶體定義初始化:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;

這裡採用靜態初始化:

cdev_init(&drvdata->cdev, &irfpa_fops);
drvdata->cdev.owner = THIS_MODULE;
drvdata->cdev.ops = &irfpa_fops;

初始化 cdev 後,需要把它新增到系統中去。為此可以呼叫 cdev_add() 函式。傳入 cdev 結構的指標,起始裝置編號,以及裝置編號範圍。
retval = cdev_add(&drvdata->cdev, devt, 1);

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
   p->dev = dev;
   p->count = count;
   return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}

裝置驅動程式通過呼叫cdev_add把它所管理的裝置物件的指標嵌入到一個型別為struct probe的節點之中,然後再把該節點加入到cdev_map所實現的雜湊連結串列中。
使用cdev_add註冊字元裝置前應該先呼叫register_chrdev_region或alloc_chrdev_region分配裝置號。alloc_chrdev_region申請一個動態主裝置號,並申請一系列次裝置號。baseminor為起始次裝置號,count為次裝置號的數量。登出裝置號(cdev_del)後使用unregister_chrdev_region。
核心中所有都字元裝置都會記錄在一個 kobj_map 結構的 cdev_map 變數中。這個結構的變數中包含一個散列表用來快速存取所有的物件。kobj_map() 函式就是用來把字元裝置編號和 cdev 結構變數一起儲存到 cdev_map 這個散列表裡。當後續要開啟一個字元裝置檔案時,通過呼叫 kobj_lookup() 函式,根據裝置編號就可以找到 cdev 結構變數,從而取出其中的 ops 欄位。
這裡寫圖片描述
對系統而言,當裝置驅動程式成功呼叫了cdev_add之後,就意味著一個字元裝置物件已經加入到了系統,在需要的時候,系統就可以找到它。對使用者態的程式而言,cdev_add呼叫之後,就已經可以通過檔案系統的介面呼叫到我們的驅動程式。

static const struct file_operations irfpa_fops = {
    .owner = THIS_MODULE,
    .write = irfpa_write,
    .read = irfpa_read,
    .open = irfpa_open,
    .release = irfpa_release,
};

j、retval = sysfs_create_group(&(pdev->dev.kobj), &irfpa_attr_group);
sysfs是用於表現裝置驅動模型的檔案系統,它基於ramfs。

sysfs檔案系統中提供了四類檔案的建立與管理,分別是目錄、普通檔案、軟連結檔案、二進位制檔案。目錄層次往往代表著裝置驅動模型的結構,軟連結檔案則代表著不同部分間的關係。比如某個裝置的目錄只出現在/sys/devices下,其它地方涉及到它時只好用軟連結檔案連結過去,保持了裝置唯一的例項。而普通檔案和二進位制檔案往往代表了裝置的屬性,讀寫這些檔案需要呼叫相應的屬性讀寫。
sysfs是表現裝置驅動模型的檔案系統,它的目錄層次實際反映的是物件的層次。為了配合這種目錄,linux專門提供了兩個結構作為sysfs的骨架,它們就是struct kobject和struct kset。我們知道,sysfs是完全虛擬的,它的每個目錄其實都對應著一個kobject,要想知道這個目錄下有哪些子目錄,就要用到kset。從面向物件的角度來講,kset繼承了kobject的功能,既可以表示sysfs中的一個目錄,還可以包含下層目錄。

sysfs_create_group()在kobj目錄下建立一個屬性集合,並顯示集合中的屬性檔案。如果檔案已存在,會報錯。
sysfs_update_group()在kobj目錄下建立一個屬性集合,並顯示集合中的屬性檔案。檔案已存在也不會報錯。sysfs_update_group()也用於group改動影響到檔案顯示時呼叫。
sysfs_remove_group()在kobj目錄下刪除一個屬性集合,並刪除集合中的屬性檔案。
sysfs_add_file_to_group()將一個屬性attr加入kobj目錄下已存在的的屬性集合group。
sysfs_remove_file_from_group()將屬性attr從kobj目錄下的屬性集合group中刪除。
更詳細的參考我的另一篇部落格[DEVICE_ATTR分析](http://blog.csdn.net/chuhang_zhqr/article/details/50174813)
可以使得可以在使用者空間直接對驅動的這些變數讀寫或呼叫驅動的某些函式。

通 過以上簡單的幾個步驟,就可以在終端檢視到介面了。當我們將資料 echo 到介面中時,在上層實際上完成了一次 write 操 作,對應到 kernel ,呼叫了驅動中的 “wirte”。同理,當我們cat 一個 介面時則會呼叫 “show” 。到這裡,只是簡單的建立 了 應用 層到 kernel 的橋樑,真正實現對硬體操作的,還是在 “show” 和 “set” 中完成的。

至此,已經註冊裝置驅動程式,並且對裝置的資源進行申請,對映,並設定瞭如何操作四個儲存器介面。
irfpa_regs_res使用對sys目錄下的裝置集進行echo和cat就可以對這個暫存器進行讀寫操作了。
vdma_regs_res和 vbuf_mem_res是被設定成字元型裝置了irfpa,在/dev下面,可以根據irfpa_fops的設定對該裝置介面進行操作。
xinfo_mem_res直接在probe程式中設定瞭如何操作:
drvdata->irfpa_xinfo_pinpon = irfpa_readreg(drvdata->irfpa_base_address + IRFPA_XINFO_PINPON);

在系統的目錄下,就可以找到一個字元型裝置和一些暫存器裝置集合。

k、irfpa_device_init(drvdata->irfpa_base_address, drvdata->vdma_base_address, vbuf_mem_res->start);
這是probe函式的最後一步,也是整個驅動程式的最後一步。初始化裝置暫存器,這是對裝置進行初始化操作,開機啟動時對整個裝置的控制暫存器進行初始化,控制裝置合理執行。