OV9650linux驱动程序解读

2023年 1月 28日 70点热度 0人点赞 0条评论

转自http://blog.sina.com.cn/s/blog_7a4cd1b701016bjb.html

学习了裸机OV9650的P通道LCD直接显示程序,作为这点基础开始分析OV9650在linux设备驱动程序。

我们跟踪程序按照常规方法,跟着驱动的编写脉络去读程序。

1、在程序中找到程序入口函数——加载和卸载module_init和module_exit

[c-sharp] view plaincopyprint?
module_init(camif_init);
module_exit(camif_cleanup);
通过这个入口函数我们找到了函数加载和卸载函数的定义,首先我们分析加载函数(由于函数的嵌套性,我们这里用序号来体现嵌套)

2、camif_init跟踪

[c-sharp] view plaincopyprint?
static int __init camif_init(void)
{
int ret;
struct tq2440_camif_dev pdev;
struct clk
camif_upll_clk;

printk(KERN_ALERT"initializing s3c2440 camera interface....../n");  

pdev = &camera;  

s3c2410_gpio_cfgpin(S3C2440_GPJ0, S3C2440_GPJ0_CAMDATA0);  
s3c2410_gpio_cfgpin(S3C2440_GPJ1, S3C2440_GPJ1_CAMDATA1);  
s3c2410_gpio_cfgpin(S3C2440_GPJ2, S3C2440_GPJ2_CAMDATA2);  
s3c2410_gpio_cfgpin(S3C2440_GPJ3, S3C2440_GPJ3_CAMDATA3);  
s3c2410_gpio_cfgpin(S3C2440_GPJ4, S3C2440_GPJ4_CAMDATA4);  
s3c2410_gpio_cfgpin(S3C2440_GPJ5, S3C2440_GPJ5_CAMDATA5);  
s3c2410_gpio_cfgpin(S3C2440_GPJ6, S3C2440_GPJ6_CAMDATA6);  
s3c2410_gpio_cfgpin(S3C2440_GPJ7, S3C2440_GPJ7_CAMDATA7);  
s3c2410_gpio_cfgpin(S3C2440_GPJ8, S3C2440_GPJ8_CAMPCLK);  
s3c2410_gpio_cfgpin(S3C2440_GPJ9, S3C2440_GPJ9_CAMVSYNC);  
s3c2410_gpio_cfgpin(S3C2440_GPJ10, S3C2440_GPJ10_CAMHREF);  
s3c2410_gpio_cfgpin(S3C2440_GPJ11, S3C2440_GPJ11_CAMCLKOUT);  
s3c2410_gpio_cfgpin(S3C2440_GPJ12, S3C2440_GPJ12_CAMRESET);  

if (!request_mem_region((unsigned long)S3C2440_PA_CAMIF, S3C2440_SZ_CAMIF, CARD_NAME))  
{  
    ret = -EBUSY;  
    goto error1;  
}  

camif_base_addr = (unsigned long)ioremap_nocache((unsigned long)S3C2440_PA_CAMIF, S3C2440_SZ_CAMIF);  
if (camif_base_addr == (unsigned long)NULL)  
{  
    ret = -EBUSY;  
    goto error2;  
}  

pdev->clk = clk_get(NULL, "camif");  
if (IS_ERR(pdev->clk))  
{  
    ret = -ENOENT;  
    goto error3;  
}  
clk_enable(pdev->clk);  

camif_upll_clk = clk_get(NULL, "camif-upll");  
clk_set_rate(camif_upll_clk, 24000000);  
mdelay(100);  

mutex_init(&pdev->rcmutex);  
pdev->rc = 0;  

pdev->input = 0;  

pdev->state = CAMIF_STATE_FREE;  

pdev->cmdcode = CAMIF_CMD_NONE;  
init_waitqueue_head(&pdev->cmdqueue);  

if (misc_register(&misc) < 0)  
{  
    ret = -EBUSY;  
    goto error4;  
}  
printk(KERN_ALERT"s3c2440 camif init done/n");  

// sccb_init();
CFG_WRITE(SIO_C);
CFG_WRITE(SIO_D);

High(SIO_C);  
High(SIO_D);  
WAIT_STABLE();  

hw_reset_camif();  
has_ov9650 = s3c2440_ov9650_init() >= 0;  
s3c2410_gpio_setpin(S3C2410_GPG4, 1);  
return 0;  

error4:
clk_put(pdev->clk);
error3:
iounmap((void *)camif_base_addr);
error2:
release_mem_region((unsigned long)S3C2440_PA_CAMIF, S3C2440_SZ_CAMIF);

error1:
return ret;
}
①定义Camera Interface的硬件管脚功能(即设置GPJ为功能引脚)

②初始化Camera的虚拟内存地址

request_mem_region() -- 将起始地址为[start, start+n-1]的资源插入根资源iomem_resource中。参数start是I/O内存资源的起始物理地址(是CPU的RAM物理地址空间中的物理地址),参数n指定I/O内存资源的大小。

define request_mem_region(start, n, name) /

    __request_region(&iomem_resource, (start), (n), (name))

注: 调用request_mem_region()不是必须的,但是建议使用。该函数的任务是检查申请的资源是否可用,如果可用则申请成功,并标志为已经使用,其他驱动想再申请该资源时就会失败。
③映射为虚拟内存

ioremap_nocache - 把内存映射到CPU空间

void __iomem * ioremap_nocache (unsigned long phys_addr, unsigned long size);

phys_addr 要映射的物理地址

size 要映射资源的大小

ioremap_nocache进行一系列平台相关的操作使得CPU可以通过readb/readw/readl/writeb/writew/writel等IO函数进行访问。

返回的地址不保证可以作为虚拟地址直接访问。

[译者按:在译者的使用过程种并没有出现不能作为虚拟地址直接访问的情况,可能是某些平台下的不可以吧。译者的使用平台是x86和ixp425]

这个版本的ioremap确保这些内存在CPU是不可缓冲的,如同PCI总线上现存的缓冲规则一样。注:此时在很多总线上仍有其他的缓冲和缓存。在某些特殊的驱动中,作者应当在PCI写的时候进行读取。
这对于一些控制寄存器在这种不希望复合写或者缓冲读的区域内时是非常有用的
返回的映射地址必须使用iounmap来释放。

④初始化Camera的时钟(这里通过一个例子说明函数)

如何获取FCLK, HLCK 和 PCLK的时针频率呢?

可先通过clk_get获取一个clk结构体

struct clk clk_get(struct device dev, const char id)
struct clk {
struct list_head list;
struct module
owner;
struct clk parent;
const char
name;
int id;
int usage;
unsigned long rate;
unsigned long ctrlbit;
int (enable)(struct clk , int enable);
int (set_rate)(struct clk c, unsigned long rate);
unsigned long (get_rate)(struct clk c);
unsigned long (round_rate)(struct clk c, unsigned long rate);
int (set_parent)(struct clk c, struct clk *parent);
};

再将clk_get返回的clk结构体传递给clk_get_rate,获取该时钟的频率

unsigned long clk_get_rate(struct clk *clk)

一个例子:

这里出现了另一个时针uclk,专门给usb供给时针信号。uclk是外部时针源,由s3c2410芯片的gph8/uclk管脚引入,给uart提供外部时针信号,以获取更精确地时针频率。

⑤初始化计数器和它的互斥

用互斥锁可以使线程顺序执行。互斥锁通常只允许一个线程执行一个关键部分的
代码,来同步线程。互斥锁也可以用来保护单线程代码。
⑥初始化输入的源图象

⑦初始化Camera 接口的状态

enum

{

CAMIF_STATE_FREE = 0, // not openned

CAMIF_STATE_READY = 1, // openned, but standby

CAMIF_STATE_PREVIEWING = 2, // in previewing

CAMIF_STATE_CODECING = 3 // in capturing

};

⑧初始化命令代码,和初始化等待队列头

enum

{

CAMIF_CMD_NONE = 0,

CAMIF_CMD_SFMT = 1<<0, // source image format changed.

CAMIF_CMD_WND = 1<<1, // window offset changed.

CAMIF_CMD_ZOOM = 1<<2, // zoom picture in/out

CAMIF_CMD_TFMT = 1<<3, // target image format changed.

CAMIF_CMD_P2C = 1<<4, // need camif switches from p-path to c-path

CAMIF_CMD_C2P = 1<<5, // neet camif switches from c-path to p-path

CAMIF_CMD_STOP = 1<<16 // stop capture

};

⑨注册到视频设备层

杂项设备(misc device)

杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux 内核的include/linux目录下有Miscdevice.h文件,要把自己定义的misc device从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10 ,一起归于misc device,其实misc_register就是用主标号10调用register_chrdev()的。

也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。

字符设备(char device)

使用register_chrdev(LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时,如果有多个设备使用该函数注册驱动程序,LED_MAJOR不能相同,否则几个设备都无法注册(我已验证)。如果模块使用该方式注册并且 LED_MAJOR为0(自动分配主设备号 ),使用insmod命令加载模块时会在终端显示分配的主设备号和次设备号,在/dev目录下建立该节点,比如设备leds,如果加载该模块时分配的主设备号和次设备号为253和0,则建立节点:mknod leds c 253 0。使用register_chrdev (LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时都要手动建立节点 ,否则在应用程序无法打开该设备。

这里有一个结构很关键,也是我们程序脉络的一个跟踪点if (misc_register(&misc) < 0)中的misc,我们先把整个加载程序跟踪完,在回过来跟踪这个入口点。

⑩初始化sccb传输协议,与IIC协议相似,并初始化外部摄相设备

首先将GPE14和GPE15设置成输入输出功能,将两个引脚都置成高电平,延迟

重起外部摄相硬件设备

初始化外部硬件设备:

打开电源(GPG11设置为输出模式,并给其数据为0)

检测设备的厂商ID

显示设备的产品ID

配置OV9650的内部各个寄存器,在配制寄存起开始时down(®s_mutex);在配置结束时up(®s_mutex);

将GPG4引脚数据设置为1(这个设置不知道为什么,是开启LCD电源?我把这个语句注释掉了,再编译驱动下载到开发板,用camera_test测试了一下,发现一切正常)

其他的一些出错处理,主要是对前面那些出现错误后的反操作(比如申请内存,如果出错,在出错处理中就要释放内存)。

2、misc的跟踪

[c-sharp] view plain copy print ?
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = CARD_NAME,
.fops = &camif_fops,
};

这里#define MISC_DYNAMIC_MINOR 255

define CARD_NAME "camera"

关键点是camif_fops,这个结构是字符设备驱动程序的核心,当应用程序操作设备文件时所调用的open,read,write等函数,最终会调用这个结构体中指定的对应函数

2、file_operations结构体camif_fops

[c-sharp] view plaincopyprint?
static struct file_operations camif_fops =
{
.owner = THIS_MODULE,
.open = camif_open,
.release = camif_release,
.read = camif_read,
};

由这个结构体我们知道,程序定义了三个接口函数open,release,read。因此接下来我们要具体的分跟踪这三个函数,这也是编写字符行设备驱动程序时我们主要完成的工作。

㈠ camif_open的实现

[c-sharp] view plaincopyprint?
static int camif_open(struct inode inode, struct file file)
{
struct tq2440_camif_dev pdev;
struct tq2440_camif_fh
fh;

int ret;  

if (!has_ov9650) {  
    return -ENODEV;  
}  
pdev = &camera;  

fh = kzalloc(sizeof(*fh),GFP_KERNEL); // alloc memory for filehandle  
if (NULL == fh)  
{  
    return -ENOMEM;  
}  
fh->dev = pdev;  

pdev->state = CAMIF_STATE_READY;  

init_camif_config(fh);  

ret = init_image_buffer();  // init image buffer.  
if (ret < 0)  
{  
    goto error1;  
}  

request_irq(IRQ_S3C2440_CAM_C, on_camif_irq_c, IRQF_DISABLED, "CAM_C", pdev);   // setup ISRs  
if (ret < 0)  
{  
    goto error2;  
}  

request_irq(IRQ_S3C2440_CAM_P, on_camif_irq_p, IRQF_DISABLED, "CAM_P", pdev);  
if (ret < 0)  
{  
    goto error3;  
}  

clk_enable(pdev->clk);       // and enable camif clock.  

soft_reset_camif();  

file->private_data = fh;  
fh->dev  = pdev;  
update_camif_config(fh, 0);  
return 0;  

error3:
free_irq(IRQ_S3C2440_CAM_C, pdev);
error2:
free_image_buffer();
error1:
kfree(fh);
return ret;
}

①通过has_ov9650来判断是否检测到0v9650设备的厂商ID(这个变量在程序加载时被设置)

②给设备结构体tq2440_camif_dev在内存中分配区域

③将设备结构体中的状态变量设置为CAMIF_STATE_READY

④初始化camera接口,配置相关的寄存器

⑤初始化图像的缓存区(即4个DMA通道的缓存地址)

⑥设置请求C通道,P通道的的中断

⑦使能camif的时钟,软件重起camif,将设备指针赋值给文件指针的私有数据

⑧最后更新配置(这步在这里好像也是可有可无的)

(1)init_camif_config

[c-sharp] view plaincopyprint?
static void init_camif_config(struct tq2440_camif_fh fh)
{
struct tq2440_camif_dev
pdev;

pdev = fh->dev;  

pdev->input = 0; // FIXME, the default input image format, see inputs[] for detail.  

pdev->srcHsize = 1280;   // FIXME, the OV9650's horizontal output pixels.  
pdev->srcVsize = 1024;   // FIXME, the OV9650's verical output pixels.  

pdev->wndHsize = 1280;             
pdev->wndVsize = 1024;  

pdev->coTargetHsize = pdev->wndHsize;  
pdev->coTargetVsize = pdev->wndVsize;  

pdev->preTargetHsize = 320;  
pdev->preTargetVsize = 240;  

update_camif_config(fh, CAMIF_CMD_STOP);  

}

这里有个函数update_camif_config(fh,CMAIF_CMD_STOP),用于更新camera接口的配置

static void update_camif_config (struct tq2440_camif_fh fh, u32 cmdcode)
{
struct tq2440_camif_dev
pdev;

pdev = fh->dev;  

switch (pdev->state)  
{  
case CAMIF_STATE_READY:  
    update_camif_regs(fh->dev);  // config the regs directly.  
    break;  

case CAMIF_STATE_PREVIEWING:  

         disable_irq(IRQ_S3C2440_CAM_P);  // disable cam-preview irq.  

    if (cmdcode & CAMIF_CMD_SFMT)  
    {  
        // ignore it, nothing to do now.  
    }  

    if (cmdcode & CAMIF_CMD_TFMT)  
    {  

            pdev->cmdcode |= CAMIF_CMD_TFMT;  
    }  

    if (cmdcode & CAMIF_CMD_WND)  
    {  
        pdev->cmdcode |= CAMIF_CMD_WND;  
    }  

    if (cmdcode & CAMIF_CMD_ZOOM)  
    {  
        pdev->cmdcode |= CAMIF_CMD_ZOOM;  
    }  

    if (cmdcode & CAMIF_CMD_STOP)  
    {  
        pdev->cmdcode |= CAMIF_CMD_STOP;  
    }  
    enable_irq(IRQ_S3C2440_CAM_P);  // enable cam-preview irq.  

    wait_event(pdev->cmdqueue, (pdev->cmdcode==CAMIF_CMD_NONE));  // wait until the ISR completes command.  
    break;  

case CAMIF_STATE_CODECING:  

    disable_irq(IRQ_S3C2440_CAM_C);     // disable cam-codec irq.  

    if (cmdcode & CAMIF_CMD_SFMT)  
    {  
        // ignore it, nothing to do now.  
    }  

    if (cmdcode & CAMIF_CMD_TFMT)  
    {  

            pdev->cmdcode |= CAMIF_CMD_TFMT;  
    }  

    if (cmdcode & CAMIF_CMD_WND)  
    {  
        pdev->cmdcode |= CAMIF_CMD_WND;  
    }  

    if (cmdcode & CAMIF_CMD_ZOOM)  
    {  
        pdev->cmdcode |= CAMIF_CMD_ZOOM;  
    }  

    if (cmdcode & CAMIF_CMD_STOP)  
    {  
        pdev->cmdcode |= CAMIF_CMD_STOP;  
    }  
    enable_irq(IRQ_S3C2440_CAM_C);  // enable cam-codec irq.  
    wait_event(pdev->cmdqueue, (pdev->cmdcode==CAMIF_CMD_NONE));  // wait until the ISR completes command.  
    break;  

default:  
    break;  
}  

}

update_camif_regs(fh->dev);表示如果为准备状态就直接配置寄存器

static void inline update_camif_regs(struct tq2440_camif_dev * pdev)
{
if (!in_irq())
{
while(1) // wait until VSYNC is 'L'
{
barrier();
if ((ioread32(S3C244X_CICOSTATUS)&(1<<28)) == 0)
break;
}
}

update_source_fmt_regs(pdev);  
update_target_wnd_regs(pdev);  
update_target_fmt_regs(pdev);  
update_target_zoom_regs(pdev);  

}

这里最后4个函数是配置camera interface寄存器的函数,由于寄存器的配置过程跟裸机的配置大同小异,所以这里就不进入其中讲解了,但有一点要注意,如果要修改显示效果,我觉得可以到这里面来配置一下寄存器,修改一下参数
init_camif_buffer

static int inline init_image_buffer(void)
{
int size1, size2;
unsigned long size;
unsigned int order;

size1 = MAX_C_WIDTH * MAX_C_HEIGHT * 16 / 8;  

size2 = MAX_P_WIDTH * MAX_P_HEIGHT * 16 / 8;  

size = (size1 > size2)?size1:size2;  

order = get_order(size);  
img_buff[0].order = order;  
img_buff[0].virt_base = __get_free_pages(GFP_KERNEL|GFP_DMA, img_buff[0].order);  
if (img_buff[0].virt_base == (unsigned long)NULL)  
{  
    goto error0;  
}  
img_buff[0].phy_base = img_buff[0].virt_base - PAGE_OFFSET + PHYS_OFFSET;   // the DMA address.  

img_buff[1].order = order;  
img_buff[1].virt_base = __get_free_pages(GFP_KERNEL|GFP_DMA, img_buff[1].order);  
if (img_buff[1].virt_base == (unsigned long)NULL)  
{  
    goto error1;  
}  
img_buff[1].phy_base = img_buff[1].virt_base - PAGE_OFFSET + PHYS_OFFSET;   // the DMA address.  

img_buff[2].order = order;  
img_buff[2].virt_base = __get_free_pages(GFP_KERNEL|GFP_DMA, img_buff[2].order);  
if (img_buff[2].virt_base == (unsigned long)NULL)  
{  
    goto error2;  
}  
img_buff[2].phy_base = img_buff[2].virt_base - PAGE_OFFSET + PHYS_OFFSET;   // the DMA address.  

img_buff[3].order = order;  
img_buff[3].virt_base = __get_free_pages(GFP_KERNEL|GFP_DMA, img_buff[3].order);  
if (img_buff[3].virt_base == (unsigned long)NULL)  
{  
    goto error3;  
}  
img_buff[3].phy_base = img_buff[3].virt_base - PAGE_OFFSET + PHYS_OFFSET;   // the DMA address.  

invalid_image_buffer();  

return 0;  

error3:
free_pages(img_buff[2].virt_base, order);
img_buff[2].phy_base = (unsigned long)NULL;
error2:
free_pages(img_buff[1].virt_base, order);
img_buff[1].phy_base = (unsigned long)NULL;
error1:
free_pages(img_buff[0].virt_base, order);
img_buff[0].phy_base = (unsigned long)NULL;
error0:
return -ENOMEM;
}

(二) camif_read的实现

static ssize_t camif_read(struct file file, char __user data, size_t count, loff_t ppos)
{
int i;
struct tq2440_camif_fh
fh;
struct tq2440_camif_dev * pdev;

fh = file->private_data;  
pdev = fh->dev;  

if (start_capture(pdev, 0) != 0)  
{  
    return -ERESTARTSYS;  
}  

disable_irq(IRQ_S3C2440_CAM_C);  
disable_irq(IRQ_S3C2440_CAM_P);  
for (i = 0; i < 4; i++)  
{  
    if (img_buff[i].state != CAMIF_BUFF_INVALID)  
    {  
        copy_to_user(data, (void *)img_buff[i].virt_base, count);  
        img_buff[i].state = CAMIF_BUFF_INVALID;  
    }  
}  
enable_irq(IRQ_S3C2440_CAM_P);  
enable_irq(IRQ_S3C2440_CAM_C);  

return count;  

①开始图像捕捉,参数表示是捕捉视频流还是仅仅捕捉一张图片

②关闭P通道和C通道的中断

③通过一个for循环语句判断数据存放地址的状态,标志为有效,则把数据从内核复制到用户空间,并将标志设置为无效

④使能P通道和C通道的中断

(1)start_capture

[c-sharp] view plaincopyprint?

static int start_capture(struct tq2440_camif_dev * pdev, int stream)
{
int ret;

u32 ciwdofst;  
u32 ciprscctrl;  
u32 ciimgcpt;  

ciwdofst = ioread32(S3C244X_CIWDOFST);  
ciwdofst    |= (1<<30)    // Clear the overflow indication flag of input CODEC FIFO Y  
    |(1<<15)      // Clear the overflow indication flag of input CODEC FIFO Cb  
    |(1<<14)      // Clear the overflow indication flag of input CODEC FIFO Cr  
    |(1<<13)      // Clear the overflow indication flag of input PREVIEW FIFO Cb  
    |(1<<12);     // Clear the overflow indication flag of input PREVIEW FIFO Cr  
iowrite32(ciwdofst, S3C244X_CIWDOFST);  

ciprscctrl = ioread32(S3C244X_CIPRSCCTRL);  
ciprscctrl |= 1<<15;  // preview scaler start  
iowrite32(ciprscctrl, S3C244X_CIPRSCCTRL);  

pdev->state = CAMIF_STATE_PREVIEWING;  

ciimgcpt = (1<<31)        // camera interface global capture enable  
     |(1<<29);        // capture enable for preview scaler.  
iowrite32(ciimgcpt, S3C244X_CIIMGCPT);  

ret = 0;  
if (stream == 0)  
{  
    pdev->cmdcode = CAMIF_CMD_STOP;  

    ret = wait_event_interruptible(pdev->cmdqueue, pdev->cmdcode == CAMIF_CMD_NONE);  
}  
return ret;  

}

⑴配置窗口移位寄存器,清空各个FIFO
⑵配置预览通道主框控制寄存器,预览模式开始
⑶将设备接口体的状态标志标识为CAMIF_STATE_PREVIEWING
⑷配置图像捕捉使能寄存器,图像捕捉全局使能,预览通道捕捉使能
⑸一个判断语句,如果stream为0,则设备结构体的命令变量变为CAMIF_CMD_STOP,并设置一个等待中断队列
camif_release的实现

static int camif_release(struct inode inode, struct file file)
{
struct tq2440_camif_fh fh;
struct tq2440_camif_dev
pdev;

fh = file->private_data;  
pdev = fh->dev;  

    clk_disable(pdev->clk);          // stop camif clock  

    free_irq(IRQ_S3C2440_CAM_P, pdev);  // free camif IRQs  

    free_irq(IRQ_S3C2440_CAM_C, pdev);  

    free_image_buffer();            // and free image buffer  

return 0;  

}

①关闭时钟,释放P通道中断,释放C通道中断,释放4个DMA缓存
5、卸载函数camif_cleanup

相对与所有的程序块,这部分应该是最简单的,基本就是释放一些资源,前面的一些反操作

static void __exit camif_cleanup(void)

{

struct tq2440_camif_dev *pdev;

// sccb_cleanup();

CFG_READ(SIO_C);

CFG_READ(SIO_D);

pdev = &camera;

misc_deregister(&misc);

clk_put(pdev->clk);

iounmap((void *)camif_base_addr);

release_mem_region((unsigned long)S3C2440_PA_CAMIF, S3C2440_SZ_CAMIF);

printk(KERN_ALERT"tq2440_camif: module removed/n");

}

6、总结

由上面的程序跟踪我们发现字符设备程序的主体就是file_operation的填充,上面很多内嵌的函数都没有粘贴出来,我不敢说不重要,实际是很多程序嵌来嵌去的很容易把我整蒙,所以只是大致的读了一意思(能明白这块是做什么的以及如果我要修改程序我应该明白修改什么地方就行)。

本文来自:https://blog.duhbb.com

本文链接地址:OV9650linux驱动程序解读,英雄不问来路,转载请注明出处,谢谢。

有话想说:那就赶紧去给我留言吧。

rainbow

这个人很懒,什么都没留下

文章评论