今年上半年我對市場常見的vimicro 網路攝影機移植了Linux下的驅動至在研的一款嵌入式開發板,開發環境為:
OS:Linux2.6.9
Compile:cross 3.3.2
CPU:Intel PXA270
Camera Type :Vimicro ZC301P +HV7131R
使用的驅動源碼基於自由軟體spca5xx。基於源碼作了些小改動。記錄一下。
Linux下的USB驅動架構
Linux下完整的USB驅動程式必須包括3個模組:USB core,USB 主控制器驅動,USB裝置驅動。三者的關係如下:
其中:
Linux Core封裝了支援USB主控制器和USB裝置驅動的API,通過一系列的資料結構、宏、函數來抽象化裝置硬體並掩蔽硬體具體細節,使得Linux系統對USB裝置的訪問採用如同一般檔案訪問類似的介面函數,如此實現了硬體無關。“裝置即是檔案”。
USB HC(Host Controller)全面負責主機端對USB的資訊傳輸分工,對下註冊功能和相關資料結構;對上提供了裝置驅動與USB Core資料轉送的橋樑。USB上的即時資料流資料轉送通常通過URB(USB Request Block)的資料結構實現,每一次資料轉送中,裝置驅動建立URB,提交給USB HC,USB HC負責完成此次與裝置端的URB互動。
USB驅動程式的結構
struct usb_driver spca5xx_driver
struct video_device spca50x_template
struct file_operations spca5xx_fops
模組初始化與卸載
spca5xx_init()模組初始化;spca5xx_disconnect()模組卸載;spca5xx_probe() 物理裝置尋找、匹配、配置,並向核心註冊video裝置。
系統調用介面
spca5xx_open()完成裝置的開啟和初始化,並初始化解碼器模組,spca5xx_close()完成裝置的關閉;
spca5xx_read()完成資料的讀取,其主要的工作就是將資料由核心空間傳送到進程使用者空間;
spca5xx_mmap()實現將裝置記憶體映射到使用者進程的地址空間的功能;
spca5xx_ioctl()實現檔案資訊的擷取功能。
資料轉送
spca50x_reg_write
網路攝影機資料轉送過程
USB匯流排上資訊傳輸類型可分成控制、中斷、等時、成塊四種:其中控制型主要用於裝置的“配置”與控制;中斷型主要用於主機對USB裝置的周期性查詢;即時性使用與即時的音視頻訊號傳輸;成塊型用於資訊量大,沒有很強的時間要求,但要求可靠傳遞的應用。對網路攝影機而言,通常採用的是即時傳輸。USB裝置端提供若干通訊連接埠(port),主機與連接埠間建立起邏輯上的通訊管道,進行資料轉送。在網路攝影機的裝置初始化階段,主機與0號連接埠通訊,驅動程式檢測網路攝影機型號和所有連接埠資訊;進入資料轉送階段,主機與探測到的即時輸入連接埠建立通訊管道,即時回傳網路攝影機拍攝到的映像資料。
USB主機控制器把每秒匯流排時間分成1024個固定大小的frame,每個frame佔據1ms的時間頻寬並以此為時間單位遞增,同時對應一個等時互動隊列。主機負責把USB匯流排上回傳的等時映像資料流量分配給各frame,USB主機控制器每一秒鐘掃描一遍所有frame,依次讀入各frame攜帶的等時資料包至核心緩衝區中。應用程式通過記憶體映射,在使用者空間直接讀取核心緩衝區內容,存入Linux顯存幀緩衝(framebuffer)中,從而在顯示屏中出現映像資料。
筆者的實驗網路攝影機為Vimicro ZC301P,sensor是HV7131R,驅動程式中每秒鐘進行2次URB傳輸,每個URB攜帶16個1023位元組的資料包,這樣每秒鐘所能得到的映像資料大小為32Kbyte。對RGB565、大小320×240、16位色彩深度的映像格式,在PXA270處理器的嵌入式開發板上測試,幀速達到每秒17幀,完全滿足嵌入式網路即時視頻會議的需要。
提高網路攝影機工作品質的幾種方法
雙URB
雙幀緩衝
V4L協議改進
註:最近重新分析SPCA5XX的源碼,網路上偶得一師兄的文章,對SPCA5XX分析得很是詳盡,看後頗有心得,特轉貼於此,以示謝意。
Spac5xx的實現是按照標準的USB VIDEO裝置的驅動架構編寫(其具體的驅動架構可參照/usr/src/linux/drivers/usb/usbvideo.c檔案),整個來源程式由四個主體部分組成:裝置模組的初始化模組和卸載模組,上層軟體介面模組,資料轉送模組。具體的模組分析如下:
初始化裝置模組:
該驅動採用了顯式的模組初始化和消除函數,即調用module_init來初始化一個模組,並在卸載時調用moduel-exit函數(此二函數在2.3.13核心開始支援)。其具體實現如下:
1.模組初始化:
module_init (usb_spca5xx_init);
static int __init
usb_spca5xx_init (void)
{
#ifdef CONFIG_PROC_FS
proc_spca50x_create (); //建立PROC裝置檔案
#endif /* CONFIG_PROC_FS */
if (usb_register (&spca5xx_driver) < 0) //註冊USB裝置驅動
return -1;
info ("spca5xx driver %s registered", version);
return 0;
}
2.模組卸載:
module_exit (usb_spca5xx_exit);
tatic void __exit
usb_spca5xx_exit (void)
{
usb_deregister (&spca5xx_driver); //登出USB裝置驅動
info ("driver spca5xx deregistered");
#ifdef CONFIG_PROC_FS
proc_spca50x_destroy (); //撤消PROC裝置檔案
#endif /* CONFIG_PROC_FS */
}
關鍵資料結構: //USB驅動結構,隨插即用功能的實現
static struct usb_driver spca5xx_driver = {
"spca5xx",
spca5xx_probe, //註冊裝置自我偵測功能
spca5xx_disconnect, //註冊裝置自我斷開功能
{NULL,NULL}
};
用兩個函數調用spca5xx_probe 和spca5xx_disconnect來支援USB裝置的隨插即用功能:
spca5xx_probe具體實現如下:
static void *
spca5xx_probe (struct usb_device *dev, unsigned int ifnum, const struct usb_device_id *id)
{
struct usb_interface_descriptor *interface; //USB裝置介面描述符
struct usb_spca50x *spca50x; //物理裝置資料結構
int err_probe;
int i;
if (dev->descriptor.bNumConfigurations != 1) //探測裝置是不是可配置
goto nodevice;
if (ifnum > 0)
goto nodevice;
interface = &dev->actconfig->interface[ifnum].altsetting[0];
MOD_INC_USE_COUNT;
interface = &intf->altsetting[0].desc;
if (interface->bInterfaceNumber > 0)
goto nodevice;
if ((spca50x = kmalloc (sizeof (struct usb_spca50x), GFP_KERNEL)) == NULL)
//分配物理地址空間
{
err ("couldn't kmalloc spca50x struct");
goto error;
}
memset (spca50x, 0, sizeof (struct usb_spca50x));
spca50x->dev = dev;
spca50x->iface = interface->bInterfaceNumber;
if ((err_probe = spcaDetectCamera (spca50x)) < 0)
//具體物理裝置尋找,匹配廠商號,裝置號(在子程式中)
{
err (" Devices not found !! ");
goto error;
}
PDEBUG (0, "Camera type %s ", Plist[spca50x->cameratype].name
for (i = 0; i < SPCA50X_NUMFRAMES; i++)
init_waitqueue_head (&spca50x->frame[i].wq); //初始化幀等待隊列
init_waitqueue_head (&spca50x->wq); //初始化驅動等待隊列
if (!spca50x_configure (spca50x))
//物理裝置配置(主要完成感應器偵測和圖形參數配置),主要思想是給控制寄存器寫值,讀回其傳回值,以此判斷具體的感應器型號
{
spca50x->user = 0;
init_MUTEX (&spca50x->lock); //訊號量初始化
init_MUTEX (&spca50x->buf_lock);
spca50x->v4l_lock = SPIN_LOCK_UNLOCKED;
spca50x->buf_state = BUF_NOT_ALLOCATED;
}
else
{
err ("Failed to configure camera");
goto error;
}
/* Init video stuff */
spca50x->vdev = video_device_alloc (); //裝置控制塊記憶體配置
if (!spca50x->vdev)
goto error;
memcpy (spca50x->vdev, &spca50x_template, sizeof (spca50x_template));
//系統調用的掛接,在此將驅動實現的系統調用,掛到核心中
video_set_drvdata (spca50x->vdev, spca50x);
if (video_register_device (spca50x->vdev, VFL_TYPE_GRABBER, video_nr) < 0)
{ //video裝置註冊
err ("video_register_device failed");
goto error;
}
spca50x->present = 1;
if (spca50x->force_rgb)
info ("data format set to RGB");
spca50x->task.sync = 0;
spca50x->task.routine = auto_bh;
spca50x->task.data = spca50x;
spca50x->bh_requested = 0;
MOD_DEC_USE_COUNT; //增加模組使用數
return spca50x; //返回資料結構
error: //錯誤處理
if (spca50x->vdev)
{
if (spca50x->vdev->minor == -1)
video_device_release (spca50x->vdev);
else
video_unregister_device (spca50x->vdev);
spca50x->vdev = NULL;
}
if (spca50x)
{
kfree (spca50x);
spca50x = NULL;
}
MOD_DEC_USE_COUNT;
return NULL;
nodevice:
return NULL;
}
Spca5xx_disconnect的具體實現如下:
static void
spca5xx_disconnect (struct usb_device *dev, void *ptr)
{
struct usb_spca50x *spca50x = (struct usb_spca50x *) ptr;
int n;
MOD_INC_USE_COUNT; //增加模組使用數
if (!spca50x)
return;
down (&spca50x->lock); //減少訊號量
spca50x->present = 0; //驅動卸載置0
for (n = 0; n < SPCA50X_NUMFRAMES; n++) //標示所有幀ABORTING狀態
spca50x->frame[n].grabstate = FRAME_ABORTING;
spca50x->curframe = -1;
for (n = 0; n < SPCA50X_NUMFRAMES; n++) //喚醒所有等待進程
if (waitqueue_active (&spca50x->frame[n].wq))
wake_up_interruptible (&spca50x->frame[n].wq);
if (waitqueue_active (&spca50x->wq))
wake_up_interruptible (&spca50x->wq);
spca5xx_kill_isoc(spca50x); //子函數終止URB包的傳輸
PDEBUG (3,"Disconnect Kill isoc done");
up (&spca50x->lock); //增加訊號量
while(spca50x->user) //如果還有進程在使用,進程切換
schedule();
down (&spca50x->lock);
if (spca50x->vdev)
video_unregister_device (spca50x->vdev); //登出video裝置
usb_driver_release_interface (&spca5xx_driver, //連接埠釋放
&spca50x->dev->actconfig->
interface[spca50x->iface]);
spca50x->dev = NULL;
up (&spca50x->lock);
#ifdef CONFIG_PROC_FS
destroy_proc_spca50x_cam (spca50x); //登出PROC檔案
#endif /* CONFIG_PROC_FS */
if (spca50x && !spca50x->user) //釋放記憶體空間
{
spca5xx_dealloc (spca50x);
kfree (spca50x);
spca50x = NULL;
}
MOD_DEC_USE_COUNT; //減少模組記數
PDEBUG (3, "Disconnect complete");
}
上層軟體介面模組:
該模組通過file_operations資料結構,依據V4L協議規範,實現裝置的關鍵系統調用,實現裝置檔案化的UNIX系統設計特點。作為攝相頭驅動,其功能在於資料擷取,而沒有向攝相頭輸出的功能,因此在源碼中沒有實現write系統調用。其關鍵的資料結構如下:
static struct video_device spca50x_template = {
.owner = THIS_MODULE,
.name = "SPCA5XX USB Camera",
.type = VID_TYPE_CAPTURE,
.hardware = VID_HARDWARE_SPCA5XX,
.fops = &spca5xx_fops,
};
static struct file_operations spca5xx_fops = {
.owner = THIS_MODULE,
.open = spca5xx_open, //open 功能
.release = spca5xx_close, //close 功能
.read = spca5xx_read, //read 功能
.mmap = spca5xx_mmap, //記憶體映射功能
.ioctl = spca5xx_ioctl, //檔案資訊擷取
.llseek = no_llseek, //檔案定位功能未實現
};
Open功能:
完成裝置的開啟和初始化,並初始化解碼器模組。其具體實現如下:
static int
spca5xx_open(struct video_device *vdev, int flags)
{
struct usb_spca50x *spca50x = video_get_drvdata (vdev);
int err;
MOD_INC_USE_COUNT; //增加模組記數
down (&spca50x->lock);
err = -ENODEV;
if (!spca50x->present) //檢查裝置是不是存在,有不有驅動,是不是忙
goto out;
err = -EBUSY;
if (spca50x->user)
goto out;
err = -ENOMEM;
if (spca50x_alloc (spca50x))
goto out;
err = spca50x_init_source (spca50x); //初始化感應器和解碼模組,在此函數的實現中,對每一款DSP晶片的初始化都不一樣,對中星微301P的DSP晶片的初始化在子函數zc3xx_init,其實現方法為寄存器填值。
if (err != 0){
PDEBUG (0, "DEALLOC error on spca50x_init_source/n");
up (&spca50x->lock);
spca5xx_dealloc (spca50x);
goto out2;
}
spca5xx_initDecoder(spca50x); //解碼模組初始化,其模組的具體實現採用的是huffman演算法
spca5xx_setFrameDecoder(spca50x);
spca50x->user++;
err = spca50x_init_isoc (spca50x); //初始化URB(usb request block) 包,啟動攝相頭,採用同步傳輸的方式傳送資料
if (err)
{
PDEBUG (0, " DEALLOC error on init_Isoc/n");
spca50x->user--;
spca5xx_kill_isoc (spca50x);
up (&spca50x->lock);
spca5xx_dealloc (spca50x);
goto out2;
}
spca50x->brightness = spca50x_get_brghtness (spca50x) << 8;
spca50x->whiteness = 0;
out:
up (&spca50x->lock);
out2:
if (err)
MOD_DEC_USE_COUNT;
if (err)
{
PDEBUG (2, "Open failed");
}
else
{
PDEBUG (2, "Open done");
}
return err;
}
2.Close功能:
完成裝置的關閉,其具體過程是:
static void
spca5xx_close( struct video_device *vdev)
{
struct usb_spca50x *spca50x =vdev->priv;
int i;
PDEBUG (2, "spca50x_close");
down (&spca50x->lock); //參數設定
spca50x->user--;
spca50x->curframe = -1;
if (spca50x->present) //present:是或有驅動載入
{
spca50x_stop_isoc (spca50x); //停止攝相頭工作和資料包發送
spcaCameraShutDown (spca50x); //關閉攝相頭,由子函數spca50x_stop_isoc完成
for (i = 0; i < SPCA50X_NUMFRAMES; i++) //喚醒所有等待進程
{
if (waitqueue_active (&spca50x->frame[i].wq))
wake_up_interruptible (&spca50x->frame[i].wq);
}
if (waitqueue_active (&spca50x->wq))
wake_up_interruptible (&spca50x->wq);
}
up (&spca50x->lock);
spca5xx_dealloc (spca50x); //回收記憶體空間
PDEBUG(2,"Release ressources done");
MOD_DEC_USE_COUNT;
}
Read功能:
完成資料的讀取,其主要的工作就是將資料由核心空間傳送到進程使用者空間。
static long
spca5xx_read(struct video_device *dev, char * buf, unsigned long
count,int noblock)
{
struct usb_spca50x *spca50x = video_get_drvdata (dev);
int i;
int frmx = -1;
int rc;
volatile struct spca50x_frame *frame;
if (down_interruptible(&spca50x->lock)) //擷取訊號量
return -EINTR;
if (!dev || !buf){ //判斷裝置情況
up(&spca50x->lock);
return -EFAULT;
}
if (!spca50x->dev){
up(&spca50x->lock);
return -EIO;
}
if (!spca50x->streaming){
up(&spca50x->lock);
return -EIO;
}
if((rc = wait_event_interruptible(spca50x->wq, //在指定的隊列上睡眠,直到參數2的條件為真
spca50x->frame[0].grabstate == FRAME_DONE ||
spca50x->frame[1].grabstate == FRAME_DONE ||
spca50x->frame[2].grabstate == FRAME_DONE ||
spca50x->frame[3].grabstate == FRAME_DONE ))){
up(&spca50x->lock);
return rc;
}
for (i = 0; i < SPCA50X_NUMFRAMES; i++) //當資料到來
if (spca50x->frame[i].grabstate == FRAME_DONE) //標識資料已到
frmx = i;
if (frmx < 0)
{
PDEBUG (2, "Couldnt find a frame ready to be read.");
up(&spca50x->lock);
return -EFAULT;
}
frame = &spca50x->frame[frmx];
PDEBUG (2, "count asked: %d available: %d", (int) count,
(int) frame->scanlength);
if (count > frame->scanlength)
count = frame->scanlength;
if ((i = copy_to_user (buf, frame->data, count))) //實現使用者空間和核心空間的資料拷貝
{
PDEBUG (2, "Copy failed! %d bytes not copied", i);
up(&spca50x->lock);
return -EFAULT;
}
/* Release the frame */
frame->grabstate = FRAME_READY; //標識資料已空
up(&spca50x->lock);
return count; //返回拷貝的資料數
}
Mmap功能:
實現將裝置記憶體映射到使用者進程的地址空間的功能,其關鍵函數是remap_page_range,其具體實現如下:
static int
spca5xx_mmap(struct video_device *dev,const char *adr, unsigned long size)
{
unsigned long start=(unsigned long) adr;
struct usb_spca50x *spca50x = dev->priv;
unsigned long page, pos;
if (spca50x->dev == NULL)
return -EIO;
PDEBUG (4, "mmap: %ld (%lX) bytes", size, size);
if (size >
(((SPCA50X_NUMFRAMES * MAX_DATA_SIZE) + PAGE_SIZE - 1) & ~(PAGE_SIZE -1)))
return -EINVAL;
if (down_interruptible(&spca50x->lock)) //擷取訊號量
return -EINTR;
pos = (unsigned long) spca50x->fbuf;
while (size > 0) //迴圈實現記憶體映射
{
page = kvirt_to_pa (pos);
if (remap_page_range (start, page, PAGE_SIZE, PAGE_SHARED)){ //實現記憶體映射
up(&spca50x->lock);
return -EAGAIN; }
start += PAGE_SIZE;
pos += PAGE_SIZE;
if (size > PAGE_SIZE)
size -= PAGE_SIZE;
else
size = 0;
}
up(&spca50x->lock); //釋放訊號量
return 0;
}
Ioctl功能:
實現檔案資訊的擷取功能,
static int
spca5xx_ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
struct video_device *vdev = file->private_data;
struct usb_spca50x *spca50x = vdev->priv;
int rc;
if (down_interruptible(&spca50x->lock)) //擷取訊號量
return -EINTR;
rc = video_usercopy (inode, file, cmd, arg, spca5xx_do_ioctl); //將資訊傳送到使用者進程,其關鍵函數實現spca5xx_do_ioctl
up(&spca50x->lock);
return rc;
}
spca5xx_do_ioctl函數的實現依賴於不同的硬體,本驅動為了支援多種晶片,實現程式過於煩瑣,其主要思想是通過copy_to_user(arg,b,sizeof(struct video_capability)函數將裝置資訊傳遞給使用者進程。
資料轉送模組:
來源程式採用tasklet來實現同步快速傳遞資料,並通過spcadecode.c上的軟體解碼模組實現圖形資訊的解碼。此模組的進入點掛節在spca_open函數中,其具體的函數為spca50x_init_isoc。當裝置被開啟時,同步傳輸資料也已經開始,並通過spca50x_move_data函數將資料傳遞給驅動程式,驅動程式通過輪詢的辦法實現對資料的訪問。
void
outpict_do_tasklet (unsigned long ptr)
{
int err;
struct spca50x_frame *taskletframe = (struct spca50x_frame *) ptr;
taskletframe->scanlength = taskletframe->highwater - taskletframe->data;
PDEBUG (2, "Tasklet ask spcadecoder hdrwidth %d hdrheight %d method %d",
taskletframe->hdrwidth, taskletframe->hdrheight,
taskletframe->method);
err = spca50x_outpicture (taskletframe); //輸出處理過的圖片資料
if (err != 0)
{
PDEBUG (0, "frame decoder failed (%d)", err);
taskletframe->grabstate = FRAME_ERROR;
}
else
{
taskletframe->grabstate = FRAME_DONE;
}
if (waitqueue_active (&taskletframe->wq)) //如果有進程等待,喚醒等待進程
wake_up_interruptible (&taskletframe->wq);
}
值得一提的是spcadecode.c上解碼模組將原始壓縮圖形資料流yyuyv,yuvy, jpeg411,jpeg422解碼為RGB圖形,但此部分解壓縮演算法的實現也依賴於壓縮的格式,歸根結底依賴於DSP(數文書處理晶片)中的硬體壓縮演算法。
四.USB CORE的支援:
LINUX下的USB裝置對下層硬體的操作依靠系統實現的USB CORE層,USB CORE對上層驅動提供了眾多函數介面如:usb_control_msg,usb_sndctrlpipe等,其中最典型的使用為源碼中對USB端點寄存器的讀寫函數spca50x_reg_write和spca50x_reg_read等,具體實現如下:(舉spca50x_reg_write的實現,其他類似)
static int spca50x_reg_write(struct usb_device *dev,__u16 reg,__u16 index,
__u16 value)
{
int rc;
rc = usb_control_msg(dev, //通過USB CORE提供的介面函數設定寄存器的值
usb_sndctrlpipe(dev, 0),
reg,
USB_TYPE_VENDOR | USB_RECIP_DEVICE,
value, index, NULL, 0, TimeOut);
PDEBUG(5, "reg write: 0x%02X,0x%02X:0x%02X, 0x%x", reg, index, value, rc);
if (rc < 0)
err("reg write: error %d", rc);
return rc;
}