1. 程式人生 > 實用技巧 >Linux USB 子系統(三)—— 分析USB 儲存驅動程式

Linux USB 子系統(三)—— 分析USB 儲存驅動程式

前面學習了USB驅動的一些基礎概念與重要的資料結構,那麼究竟如何編寫一個USB 驅動程式呢?編寫與一個USB裝置驅動程式的方法和其他匯流排驅動方式類似,驅動程式把驅動程式物件註冊到USB子系統中,稍後再使用製造商和裝置標識來判斷是否安裝了硬體。當然,這些製造商和裝置標識需要我們編寫進USB 驅動程式中。

USB 驅動程式依然遵循裝置模型 —— 匯流排、裝置、驅動。和I2C 匯流排裝置驅動編寫一樣,所有的USB驅動程式都必須建立的主要結構體是 struct usb_driver,它們向USB 核心程式碼描述了USB 驅動程式。

"./drivers/usb/usb-skeleton.c"是核心提供給usb裝置驅動開發者的海量儲存usb裝置的模板程式, 程式不長, 通用性卻很強,十分經典, 深入理解這個檔案可以幫助我們更好的理解usb子系統以及usb裝置驅動框架, 寫出更好的usb海量儲存裝置驅動。


一、註冊USB驅動程式

Linux的裝置驅動,特別是這種hotplug的USB裝置驅動,會被編譯成模組,然後在需要時掛在到核心。所以USB驅動和註冊與正常的模組註冊、解除安裝是一樣的,下面是USB驅動的註冊與解除安裝:
static struct usb_driver skel_driver = {
    .name =        "skeleton",
    .probe =    skel_probe,
    .disconnect =    skel_disconnect,
    .suspend =    skel_suspend,
    .resume =    skel_resume,
    .pre_reset =    skel_pre_reset,
    .post_reset =    skel_post_reset,
    .id_table =    skel_table,
    .supports_autosuspend = 1,
};

module_usb_driver(skel_driver);

MODULE_LICENSE("GPL v2");

1.module_usb_driver
其中module_usb_driver是對usb_register和usb_deregister的封裝的巨集,用於註冊和解除安裝usb驅動。
/**
 * module_usb_driver() - Helper macro for registering a USB driver
 * @__usb_driver: usb_driver struct
 *
 * Helper macro for USB drivers which do not do anything special in module
 * init/exit. This eliminates a lot of boilerplate. Each module may only
 * use this macro once, and calling it replaces module_init() and module_exit()
 */
#define module_usb_driver(__usb_driver) \
    module_driver(__usb_driver, usb_register, \
               usb_deregister)

USB裝置驅動的模組載入函式通用的方法是在I2C裝置驅動的模組載入函式中使用usb_register(struct *usb_driver)函式新增usb_driver的工作,而在模組解除安裝函式中利用usb_deregister(struct *usb_driver)做相反的工作。 對比I2C裝置驅動中的i2c_add_driver(&i2c_driver)i2c_del_driver(&i2c_driver)

2.struct usb_driver

從程式碼看來,usb_driver需要初始化幾個欄位:

模組的名字 skeleton
probe函式 skel_probe
disconnect函式skel_disconnect



3.id_table

最重要的當然是probe函式與disconnect函式,這個在後面詳細介紹,先談一下id_table:

id_table 是struct usb_device_id型別,包含了一列該驅動程式可以支援的所有不同型別的USB裝置。如果沒有設定該變數,USB驅動程式中的探測回撥該函式將不會被呼叫。對比I2C中struct i2c_device_id *id_table,一個驅動程式可以對應多個裝置,i2c 示例:

static const struct i2c_device_id mpu6050_id[] = {
     { "mpu6050", 0},  
   	 {}    
};  

usb子系統通過裝置的production ID和vendor ID的組合或者裝置的class、subclass跟protocol的組合來識別裝置,並呼叫相關的驅動程式作處理。我們可以看看這個id_table到底是什麼東西:

/* Define these values to match your devices */
#define USB_SKEL_VENDOR_ID    0xfff0
#define USB_SKEL_PRODUCT_ID    0xfff0

/* table of devices that work with this driver */
static const struct usb_device_id skel_table[] = {
    { USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },
    { }                    /* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, skel_table);

MODULE_DEVICE_TABLE的第一個引數是裝置的型別,如果是USB裝置,那自然是usb。後面一個引數是裝置表這個裝置表的最後一個元素是空的,用於標識結束。程式碼定義了USB_SKEL_VENDOR_ID是0xfff0,USB_SKEL_PRODUCT_ID是0xfff0,也就是說,當有一個裝置接到集線器時,usb子系統就會檢查這個裝置的vendor ID和product ID,如果它們的值是0xfff0時,那麼子系統就會呼叫這個skeleton模組作為裝置的驅動。

當USB裝置接到USB控制器介面時,usb_core就檢測該裝置的一些資訊,例如生產廠商ID和產品的ID,或者是裝置所屬的class、subclass跟protocol,以便確定應該呼叫哪一個驅動處理該裝置


二、USB驅動程式中重要資料結構

我們下面所要做的就是對probe函式與disconnect函式的填充了,但是在對probe函式與disconnect函式填充之前,有必要先學習三個重要的資料結構,這在我們後面probe函式與disconnect函式中有很大的作用:


1、usb-skeleton

usb-skeleton 是一個區域性結構體,用於與端點進行通訊。下面先看一下Linux核心原始碼中的一個usb-skeleton(就是usb驅動的骨架咯),其定義的裝置結構體就叫做usb-skel:

/* Structure to hold all of our device specific stuff */
struct usb_skel {
	struct usb_device	*udev;			/* the usb device for this device */ //驅動操作的usb_device物件
	struct usb_interface	*interface;		/* the interface for this device */ //驅動操作的usb_interface物件
	struct semaphore	limit_sem;		/* limiting the number of writes in progress */ 
	struct usb_anchor	submitted;		/* in case we need to retract our submissions */
	struct urb		*bulk_in_urb;		/* the urb to read data with */ //使用的urb物件
	unsigned char           *bulk_in_buffer;	/* the buffer to receive data */ //用於接收資料的buf指標
	size_t			bulk_in_size;		/* the size of the receive buffer */  //用於接收資料的buf指標
	size_t			bulk_in_filled;		/* number of bytes in the buffer */   //標識當前緩衝區有多少有效資料的域
	size_t			bulk_in_copied;		/* already copied to user space */ //標識當前緩衝區已經被拷貝走多少資料的域
	__u8			bulk_in_endpointAddr;	/* the address of the bulk in endpoint */ //bulk裝置的輸入端點
	__u8			bulk_out_endpointAddr;	/* the address of the bulk out endpoint */ //bulk裝置的輸出端點
	int			errors;			/* the last request tanked */
	bool			ongoing_read;		/* a read is going on */ //裝置可讀標誌位,0表示可讀,1表示不可讀
	spinlock_t		err_lock;		/* lock for errors */
	struct kref		kref; //kref供核心引用計數用
	struct mutex		io_mutex;		/* synchronize I/O with disconnect */
	unsigned long		disconnected:1;
	wait_queue_head_t	bulk_in_wait;		/* to wait for an ongoing read */
};



三.probe函式分析

當platform driver與deviceplatform匹配成功的時候,就會呼叫probe。這裡呼叫skel_probe,下面流程簡化了

static int skel_probe(struct usb_interface *interface,const struct usb_device_id *id)
{
    /* allocate memory for our device state and initialize it */
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);

    /* set up the endpoint information */
    /* use only the first bulk-in and bulk-out endpoints */
    retval = usb_find_common_endpoints(interface->cur_altsetting,
            &bulk_in, &bulk_out, NULL, NULL); //現在使用的介面描述符中的端點探測函式,只使用第一個批量端點,後面詳細分析a。
    /*把相關資訊儲存到一個區域性裝置結構體中*/
    dev->bulk_in_size = usb_endpoint_maxp(bulk_in); //獲取端點的最大資料包大小
    dev->bulk_in_endpointAddr = bulk_in->bEndpointAddress; //bit0~3表示端點地址,bit8 表示方向,輸入還是輸出
    dev->bulk_in_buffer = kmalloc(dev->bulk_in_size, GFP_KERNEL); //分配用於接收資料的緩衝尺寸bulk_in_size

    dev->bulk_in_urb = usb_alloc_urb(0, GFP_KERNEL); //建立一個新的urb供驅動程式使用
    dev->bulk_out_endpointAddr = bulk_out->bEndpointAddress;
    /* save our data pointer in this interface device */
    usb_set_intfdata(interface, dev); //後面詳細分析b

    /* we can register the device now, as it is ready */
    retval = usb_register_dev(interface, &skel_class); //註冊usb裝置, 後面詳細分析c
}


三、探測和斷開函式分析

USB驅動程式指定了兩個USB核心在適當時間呼叫的函式。


1、探測函式usb_find_common_endpoints

當一個裝置被安裝而USB核心認為該驅動程式應該處理時,探測函式被呼叫;

探測函式應該檢查傳遞給他的裝置資訊確定驅動程式是否真的適合該裝置。當驅動程式因為某種原因不應控制裝置時,斷開函式被呼叫,它可以做一些清潔的工作。

系統會傳遞給探測函式的資訊是什麼呢?一個usb_interface *跟一個struct usb_device_id *作為引數。他們分別是該USB裝置的介面描述(一般會是該裝置的第0號介面,該介面的預設設定也是第0號設定)跟它的裝置ID描述(包括Vendor ID、Production ID等)。

USB驅動程式應該初始化任何可能用於控制USB裝置的區域性結構體,它還應該把所需的任何裝置相關資訊儲存到區域性結構體中。例如,USB驅動程式通常需要探測裝置對的端點地址和緩衝區大小,因為需要他們才能和端點通訊

下面具體分析探測函式做了哪些事情:

a -- 探測裝置的端點地址、緩衝區大小,初始化任何可能用於控制USB裝置的資料結構

下面是一個例項程式碼,他們探測批量型別的IN和OUT端點

具體流程如下:

該程式碼塊首先迴圈訪問該介面中存在的每一個端點,賦予該端點結構體的區域性指標以使稍後的訪問更加容易

int usb_find_common_endpoints(struct usb_host_interface *alt,
        struct usb_endpoint_descriptor **bulk_in,
        struct usb_endpoint_descriptor **bulk_out,
        struct usb_endpoint_descriptor **int_in,
        struct usb_endpoint_descriptor **int_out)
{
    for (i = 0; i < alt->desc.bNumEndpoints; ++i) {
        epd = &alt->endpoint[i].desc;
        if (match_endpoint(epd, bulk_in, bulk_out, int_in, int_out))
            return 0;
    }

我們有了一個端點,我們測定該端點型別是否批量,這首先通過USB_ENDPOINT_XFERTYPE_MASK 位掩碼來取bmAttributes變數的值,然後檢查它是否和USB_ENDPOINT_XFER_BULK 的值匹配來完成。然後檢視該端點的方向是否為IN。這可以通過檢查位掩碼 USB_DIR_IN 是否包含在bEndpointAddress 端點變數中來確定。

static bool match_endpoint(struct usb_endpoint_descriptor *epd,
        struct usb_endpoint_descriptor **bulk_in,
        struct usb_endpoint_descriptor **bulk_out,
        struct usb_endpoint_descriptor **int_in,
        struct usb_endpoint_descriptor **int_out)
{
    switch (usb_endpoint_type(epd)) { //獲取端點的傳輸型別
    case USB_ENDPOINT_XFER_BULK:  //如果是批量傳輸型別端點
        if (usb_endpoint_dir_in(epd)) { //檢視該端點的方向是否為IN
            if (bulk_in && !*bulk_in) { //這裡是bulk_in是不能是空,不然就是非法指標了。*bulk_in需要空,我們這裡只是用第一個ep的資訊,後面的epx都不需要了
                *bulk_in = epd;
                break;
            }
        } else {
            if (bulk_out && !*bulk_out) {
                *bulk_out = epd;
                break;
            }
        }
		return false;

    case USB_ENDPOINT_XFER_INT: //如果是終端傳輸型別端點
        if (usb_endpoint_dir_in(epd)) { //檢視該端點的方向是否為IN
            if (int_in && !*int_in) { //因為int_in是NULL,所以我們不使用中斷ep
                *int_in = epd;
                break;
            }
        } else {
            if (int_out && !*int_out) {
                *int_out = epd;
                break;
            }
        }
        return false;
    default:
        return false;
    }
	return (!bulk_in || *bulk_in) && (!bulk_out || *bulk_out) &&
            (!int_in || *int_in) && (!int_out || *int_out);
}

如果這些都通過了,驅動程式就知道它已經發現了正確的端點型別,可以把該端點相關的資訊儲存到一個區域性結構體中,就是我們前面的usb_skel ,以便稍後使用它和端點進行通訊.


b -- 把已經初始化資料結構的指標儲存到介面裝置中

接下來的工作是向系統註冊一些以後會用的的資訊。首先我們來說明一下usb_set_intfdata()他向核心註冊一個data,這個data的結構可以是任意的,這段程式向核心註冊了一個usb_skel結構,就是我們剛剛看到的被初始化的那個,這個data可以在以後用usb_get_intfdata來得到

usb_set_intfdata(interface, dev);


c -- 註冊USB裝置

如果USB驅動程式沒有和處理裝置與使用者互動(例如輸入、tty、視訊等)的另一種型別的子系統相關聯,驅動程式可以使用USB主裝置號,以便在使用者空間使用傳統的字元驅動程式介面。如果要這樣做,USB驅動程式必須在探測函式中呼叫 usb_resgister_dev 函式來把設備註冊到USB核心。只要該函式被呼叫,就要確保裝置和驅動程式都處於可以處理使用者訪問裝置的要求的恰當狀態

retval = usb_register_dev(interface, &skel_class);

skel_class結構。這個結構又是什麼?我們就來看看這到底是個什麼東西:

/*
 * usb class driver info in order to get a minor number from the usb core,
 * and to have the device registered with the driver core
 */
static struct usb_class_driver skel_class = {
    .name =        "skel%d",
    .fops =        &skel_fops,
    .minor_base =    USB_SKEL_MINOR_BASE,
};

它其實是一個系統定義的結構,裡面包含了一名字、一個檔案操作結構體還有一個次裝置號的基準值。事實上它才是定義真正完成對裝置IO操作的函式。所以他的核心內容應該是skel_fops。

因為usb裝置可以有多個interface,每個interface所定義的IO操作可能不一樣,所以向系統註冊的usb_class_driver要求註冊到某一個interface,而不是device,因此,usb_register_dev的第一個引數才是interface,而第二個引數就是某一個usb_class_driver。

通常情況下,linux系統用主裝置號來識別某類裝置的驅動程式,用次裝置號管理識別具體的裝置,驅動程式可以依照次裝置號來區分不同的裝置,所以,這裡的次裝置號其實是用來管理不同的interface的,但由於這個範例只有一個interface,在程式碼上無法求證這個猜想。

static const struct file_operations skel_fops = {
    .owner =    THIS_MODULE,
    .read =        skel_read,
    .write =    skel_write,
    .open =        skel_open,
    .release =    skel_release,
    .flush =    skel_flush,
    .llseek =    noop_llseek,
};


2、斷開函式

當裝置被拔出集線器時,usb子系統會自動地呼叫disconnect,他做的事情不多,最重要的是登出class_driver(交還次裝置號)和interface的data,取消:

	/* give back our minor */
    usb_deregister_dev(interface, &skel_class);

    /* prevent more I/O from starting */
    mutex_lock(&dev->io_mutex);
    dev->disconnected = 1;
    mutex_unlock(&dev->io_mutex);

    usb_kill_urb(dev->bulk_in_urb); //取消傳輸請求並等待它完成
    usb_kill_anchored_urbs(&dev->submitted) //取消全部傳送請求,
        
    /* decrement our usage count */
    kref_put(&dev->kref, skel_delete);


四、USB請求塊

USB 裝置驅動程式碼通過urb和所有的 USB 裝置通訊。urb用 struct urb 結構描述(include/linux/usb.h )。

urb 以一種非同步的方式同一個特定USB裝置的特定端點發送或接受資料。一個 USB 裝置驅動可根據驅動的需要,分配多個 urb 給一個端點或重用單個 urb 給多個不同的端點。裝置中的每個端點都處理一個 urb 佇列, 所以多個 urb 可在佇列清空之前被髮送到相同的端點。

一個 urb 的典型生命迴圈如下:

(1)被建立;
(2)被分配給一個特定 USB 裝置的特定端點;
(3)被提交給 USB 核心;
(4)被 USB 核心提交給特定裝置的特定 USB 主機控制器驅動;
(5)被 USB 主機控制器驅動處理, 並傳送到裝置;
(6)以上操作完成後,USB主機控制器驅動通知 USB 裝置驅動。


urb 也可被提交它的驅動在任何時間取消;如果裝置被移除,urb 可以被USB核心取消。urb 被動態建立幷包含一個內部引用計數,使它們可以在最後一個使用者釋放它們時被自動釋放。

struct urb
{
    /* 私有的:只能由usb核心和主機控制器訪問的欄位 */
    struct kref kref; /*urb引用計數 */
    spinlock_t lock; /* urb鎖 */
    void *hcpriv; /* 主機控制器私有資料 */
    int bandwidth; /* int/iso請求的頻寬 */
    atomic_t use_count; /* 併發傳輸計數 */
    u8 reject; /* 傳輸將失敗*/

    /* 公共的: 可以被驅動使用的欄位 */
    struct list_head urb_list; /* 連結串列頭*/
    struct usb_device *dev; /* 關聯的usb裝置 */
    unsigned int pipe; /* 管道資訊 */
    int status; /* urb的當前狀態 */
    unsigned int transfer_flags; /* urb_short_not_ok | ...*/
    void *transfer_buffer; /* 傳送資料到裝置或從裝置接收資料的緩衝區 */
    dma_addr_t transfer_dma; /*用來以dma方式向裝置傳輸資料的緩衝區 */
    int transfer_buffer_length;/*transfer_buffer或transfer_dma 指向緩衝區的大小 */
    int actual_length; /* urb結束後,傳送或接收資料的實際長度 */
    unsigned char *setup_packet; /* 指向控制urb的設定資料包的指標*/
    dma_addr_t setup_dma; /*控制urb的設定資料包的dma緩衝區*/
    int start_frame; /*等時傳輸中用於設定或返回初始幀*/
    int number_of_packets; /*等時傳輸中等時緩衝區資料 */
    int interval; /* urb被輪詢到的時間間隔(對中斷和等時urb有效) */
    int error_count;  /* 等時傳輸錯誤數量 */
    void *context; /* completion函式上下文 */
    usb_complete_t complete; /* 當urb被完全傳輸或發生錯誤時,被呼叫 */
    struct usb_iso_packet_descriptor iso_frame_desc[0];
    /*單個urb一次可定義多個等時傳輸時,描述各個等時傳輸 */
};


1、建立和登出 urb

struct urb 結構不能靜態建立,必須使用 usb_alloc_urb 函式建立. 函式原型:

struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);
//int iso_packets : urb 包含等時資料包的數目。如果不使用等時urb,則為0
//gfp_t mem_flags : 與傳遞給 kmalloc 函式呼叫來從核心分配記憶體的標誌型別相同
//返回值          : 如果成功分配足夠記憶體給 urb , 返回值為指向 urb 的指標. 如果返回值是 NULL, 則在 USB 核心中發生了錯誤, 且驅動需要進行適當清理

如果驅動已經對 urb 使用完畢, 必須呼叫 usb_free_urb 函式,釋放urb。函式原型:

void usb_free_urb(struct urb *urb);
//struct urb *urb : 要釋放的 struct urb 指標


2、初始化 urb

/*中斷 urb 初始化函式*/
static inline void usb_fill_int_urb(struct urb *urb,                                                                                                       
                 struct usb_device *dev,
                 unsigned int pipe,
                 void *transfer_buffer,
                 int buffer_length,
                 usb_complete_t complete_fn,
                 void *context,
                 int interval);

/*批量 urb 初始化函式*/
static inline void usb_fill_bulk_urb(struct urb *urb,
                 struct usb_device *dev,
                 unsigned int pipe,
                 void *transfer_buffer,
                 int buffer_length,
                 usb_complete_t complete_fn,
                 void *context);

/*控制 urb 初始化函式*/
static inline void usb_fill_control_urb(struct urb *urb,
                    struct usb_device *dev,
                    unsigned int pipe,
                    unsigned char *setup_packet,
                    void *transfer_buffer,
                    int buffer_length,
                    usb_complete_t complete_fn,
                    void *context);

//struct urb *urb :指向要被初始化的 urb 的指標
//struct usb_device *dev :指向 urb 要傳送到的 USB 裝置.
//unsigned int pipe : urb 要被髮送到的 USB 裝置的特定端點. 必須使用前面提過的 usb_******pipe 函式建立
//void *transfer_buffer :指向外發資料或接收資料的緩衝區的指標.注意:不能是靜態緩衝,必須使用 kmalloc 來建立.
//int buffer_length :transfer_buffer 指標指向的緩衝區的大小
//usb_complete_t complete :指向 urb 結束處理例程函式指標
//void *context :指向一個小資料塊的指標, 被新增到 urb 結構中,以便被結束處理例程函式獲取使用.
//int interval :中斷 urb 被排程的間隔.
//函式不設定 urb 中的 transfer_flags 變數, 因此對這個成員的修改必須由驅動手動完成

/*等時 urb 沒有初始化函式,必須手動初始化,以下為一個例子*/
urb->dev = dev;
urb->context = uvd;
urb->pipe = usb_rcvisocpipe(dev, uvd->video_endp-1);
urb->interval = 1;
urb->transfer_flags = URB_ISO_ASAP;
urb->transfer_buffer = cam->sts_buf[i];
urb->complete = konicawc_isoc_irq;
urb->number_of_packets = FRAMES_PER_DESC;
urb->transfer_buffer_length = FRAMES_PER_DESC;
for (j=0; j < FRAMES_PER_DESC; j++) {
        urb->iso_frame_desc[j].offset = j;
        urb->iso_frame_desc[j].length = 1;
}


3、提交 urb

一旦 urb 被正確地建立並初始化, 它就可以提交給 USB 核心以傳送出到 USB 裝置. 這通過呼叫函式 usb_submit_urb 實現:

int usb_submit_urb(struct urb *urb, gfp_t mem_flags);
//struct urb *urb :指向被提交的 urb 的指標 
//gfp_t mem_flags :使用傳遞給 kmalloc 呼叫同樣的引數, 用來告訴 USB 核心如何及時分配記憶體緩衝

/*因為函式 usb_submit_urb 可被在任何時候被呼叫(包括從一箇中斷上下文), mem_flags 變數必須正確設定. 根據 usb_submit_urb 被呼叫的時間,只有 3 個有效值可用:
GFP_ATOMIC 
只要滿足以下條件,就應當使用此值:
1.呼叫者處於一個 urb 結束處理例程,中斷處理例程,底半部,tasklet或者一個定時器回撥函式.
2.呼叫者持有自旋鎖或者讀寫鎖. 注意如果正持有一個訊號量, 這個值不必要.
3.current->state 不是 TASK_RUNNING. 除非驅動已自己改變 current 狀態,否則狀態應該一直是 TASK_RUNNING .

GFP_NOIO 
驅動處於塊 I/O 處理過程中. 它還應當用在所有的儲存型別的錯誤處理過程中.

GFP_KERNEL 
所有不屬於之前提到的其他情況
*/

在 urb 被成功提交給 USB 核心之後, 直到結束處理例程函式被呼叫前,都不能訪問 urb 結構的任何成員.


4、urb結束處理例程

如果 usb_submit_urb 被成功呼叫, 並把對 urb 的控制權傳遞給 USB 核心, 函式返回 0; 否則返回一個負的錯誤程式碼. 如果函式呼叫成功, 當 urb 被結束的時候結束處理例程會被呼叫一次.當這個函式被呼叫時, USB 核心就完成了這個urb, 並將它的控制權返回給裝置驅動.

只有 3 種結束urb並呼叫結束處理例程的情況:

(1)urb 被成功傳送給裝置, 且裝置返回正確的確認.如果這樣, urb 中的status變數被設定為 0.
(2)發生錯誤, 錯誤值記錄在 urb 結構中的 status 變數.
(3)urb 從 USB 核心unlink. 這發生在要麼當驅動通過呼叫 usb_unlink_urb 或者 usb_kill_urb告知 USB 核心取消一個已提交的 urb,或者在一個 urb 已經被提交給它時裝置從系統中去除.

5、取消 urb

使用以下函式停止一個已經提交給 USB 核心的 urb:

void usb_kill_urb(struct urb *urb)
int usb_unlink_urb(struct urb *urb);

如果呼叫usb_kill_urb函式,則 urb 的生命週期將被終止. 這通常在裝置從系統移除時,在斷開回調函式(disconnect callback)中呼叫.

對一些驅動, 應當呼叫 usb_unlink_urb 函式來使 USB 核心停止 urb. 這個函式不會等待 urb 完全停止才返回. 這對於在中斷處理例程中或者持有一個自旋鎖時去停止 urb 是很有用的, 因為等待一個 urb 完全停止需要 USB 核心有使呼叫程序休眠的能力(wait_event()函式).



五.usb讀取

在probe函式中註冊了file_operations結構體skel_fops用於應用訪問驅動。應用open之後就可以呼叫read和write對裝置進行讀寫了。


1.skel_read

首先是skel_read(),這個函式是應用層讀裝置時回撥的函式,它試圖實現這樣一個功能: 如果核心緩衝區有資料就將適當的資料拷貝給應用層, 如果沒有就呼叫skel_do_read_io來向裝置請求資料。

static ssize_t skel_read(struct file *file, char *buffer, size_t count,
             loff_t *ppos)
{
    dev = file->private_data; //先將藏在file_private_data中的資源物件拿出來

    /* no concurrent readers */
    rv = mutex_lock_interruptible(&dev->io_mutex); //不能並行去讀

    /* if IO is under way, we must not touch things */ //如果IO在進行中,我們就不能讀取資料
retry:
    /*資源物件中的可讀標誌位,不可讀的時候,判斷IO是否允許阻塞,如果不允許就直接返回,允許阻塞就使用資源物件中的等待佇列頭,
     * 將程序加入等待佇列,使用的是interruptible版本的wait,如果睡眠中的程序是被中斷喚醒的,那麼rv==-1,函式直接返回。*/
    if (ongoing_io) {
        /* nonblocking IO shall not wait */
        if (file->f_flags & O_NONBLOCK) { //如果IO在進行中,不等待直接返回
            rv = -EAGAIN;
            goto exit;
        }
        /*
         * IO may take forever
         * hence wait in an interruptible state
         */
        rv = wait_event_interruptible(dev->bulk_in_wait, (!dev->ongoing_read)); //IO可能需要很長時間,因此在可中斷狀態中等待
        if (rv < 0)
            goto exit;
    }

    /*
     * if the buffer is filled we may satisfy the read
     * else we need to start IO
     */
    if (dev->bulk_in_filled) { //執行到這一行只有一個情況:裝置可讀了!如果緩衝區滿執行第一個語句塊,否則執行下面的語句塊
        /* we had read data */
        size_t available = dev->bulk_in_filled - dev->bulk_in_copied; //緩衝區滿時, 獲取可拷貝的資料的大小.
        size_t chunk = min(available, count); //在可拷貝的大小和期望拷貝的大小中取小者給chunk

        if (!available) { //可拷貝的資料為0, 而usb_skel->bulk_in_filled被置位才能進入這裡, 所以只有一種情況: 緩衝區的資料已經拷貝完了
            /*
             * all data has been used
             * actual IO needs to be done
             */
            rv = skel_do_read_io(dev, count);
            if (rv < 0)
                goto exit;
            else
                goto retry; //既然資料已經拷貝完畢, 呼叫skel_do_read_io發起請求
        }
        /*
         * data is available
         * chunk tells us how much shall be copied
         */
        if (copy_to_user(buffer,
                 dev->bulk_in_buffer + dev->bulk_in_copied,
                 chunk)) //從核心緩衝區usb_skel->bulk_in_buffer + usb_skel->bulk_in_copied開始(就是剩餘未拷貝資料的首地址)拷貝chunk byte的資料到應用層
            rv = -EFAULT;
        else
            rv = chunk;

        dev->bulk_in_copied += chunk; //更新usb_skel->bulk_in_copied的值

        /*
         * if we are asked for more than we have,
         * we start IO but don't wait
         */
        if (available < count) //如果可拷貝資料的大小<期望拷貝的大小, 那麼顯然剛才chunk=availible, 已經將所有的資料拷貝到應用層, 但是還不能滿足應用層的需求, 
            //呼叫skel_do_read_io來繼續向裝置索取資料, 當然, 索取的大小是沒滿足的部分, 即count-chunk
            skel_do_read_io(dev, count - chunk);

    } else { //usb_skel->bulk_in_filled沒有被置位, 表示核心緩衝區沒有資料, 呼叫skel_do_read_io索取資料, 當然, 索取的大小是全部資料, 即count
        /* no data in the buffer */
        rv = skel_do_read_io(dev, count); 
        if (rv < 0)
            goto exit;
        else
            goto retry;
    }

exit:
    mutex_unlock(&dev->io_mutex);
    return rv;
}

2.skel_do_read_io剛才也說了, 如果緩衝區不能滿足應用層需求的時候, 就會呼叫下面這個函式向bulk usb裝置請求資料, 得到資料後將資料放到緩衝區並將相應的標誌位置1/置0
static int skel_do_read_io(struct usb_skel *dev, size_t count)
{
    /* prepare a read */
    /*向usb核心提交一個urb, 將資源物件dev藏在urb->context中隨著urb實參傳入回撥函式, 和usb_fill_int_urb不同, usb_fill_bulk_urb註冊的時候
    需要將緩衝區首地址和請求資料的大小和urb繫結到一起一同提交, 這樣才知道向bulk裝置請求的資料的大小, bulk裝置有資料返回的時候才知道放哪.*/
    usb_fill_bulk_urb(dev->bulk_in_urb, dev->udev, usb_rcvbulkpipe(dev->udev, dev->bulk_in_endpointAddr), dev->bulk_in_buffer,
            min(dev->bulk_in_size, count), skel_read_bulk_callback, dev);
    /* tell everybody to leave the URB alone */
    spin_lock_irq(&dev->err_lock);
    dev->ongoing_read = 1; //將usb_skel->ongoing_read置1, 表示沒有資料可讀
    spin_unlock_irq(&dev->err_lock);

    /* submit bulk in urb, which means no data to deliver */
    dev->bulk_in_filled = 0; //將usb_skel->bulk_in_filled置0, 表示核心緩衝區沒有資料可讀
    dev->bulk_in_copied = 0; //將usb_skel->bulk_in_copied置0, 表示沒有任何資料已被拷貝

    /* do it */
    rv = usb_submit_urb(dev->bulk_in_urb, GFP_KERNEL); //做好準備工作之後, 命令usb核心傳送urb
}

3.skel_read_bulk_callback請求被髮出後, usb匯流排就會靜待裝置的反饋, 裝置有反饋後就會回撥urb的註冊函式, 我們看看這個回撥函式都做了什麼
static void skel_read_bulk_callback(struct urb *urb)
{
    dev->bulk_in_filled = urb->actual_length; //將表示裝置反饋的資料長度urb->actual_length賦值給usb_skel->bulk_in_filled, 表示緩衝區有資料了
    dev->ongoing_read = 0; //將usb_skel->ongoing_read置0, 表示可讀了!
 
    wake_up_interruptible(&dev->bulk_in_wait); //喚醒因為沒有資料可讀而陷入睡眠的程序
}


六.USB寫入寫入資料的思路是一樣的, 我這裡就不羅嗦了.

參考:Linux USB 驅動開發(三)—— 編寫USB 驅動程式_知秋一葉-CSDN部落格
Linux usb子系統(二) _usb-skeleton.c精析 - Abnor - 部落格園 (cnblogs.com)