如何在Linux下开发摄像头驱动
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在Linux下开发摄像头驱动相关的知识,希望对你有一定的参考价值。
在linux下所有设备都是文件。所以对摄像头的操作其实就是对文件的操作。USB摄像头的设备文件就是在/dev目录下的video0(假如只有一个摄像头)。在linux下操作摄像头就是使用v4l2对摄像头进行视频的操作,操作步骤如下1. 打开设备文件。
int fd=open(”/dev/video0″,O_RDWR);
2. 取得设备的capability,看看设备具有什么功能,比如是否具有视频输入,或者音频输入输出等。VIDIOC_QUERYCAP,struct v4l2_capability
v4l2_std_id std;
do
ret= ioctl(fd, VIDIOC_QUERYSTD, &std);
while (ret == -1 && errno == EAGAIN);
switch (std)
case V4L2_STD_NTSC:
//……
case V4L2_STD_PAL:
//……
3. 选择视频输入,一个视频设备可以有多个视频输入。VIDIOC_S_INPUT,struct v4l2_input(可不要)
4. 设置视频的制式和帧格式,制式包括PAL,NTSC,帧的格式个包括宽度和高度等。
VIDIOC_S_STD,VIDIOC_S_FMT,struct v4l2_std_id,struct v4l2_format
struct v4l2_format fmt;
memset ( &fmt, 0, sizeof(fmt) );
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 320;
fmt.fmt.pix.height = 240;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0)
printf("set format failed\n");
//return 0;
5. 向驱动申请帧缓冲,一般不超过5个。struct v4l2_requestbuffers
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof (req));
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd,VIDIOC_REQBUFS,&req) == -1)
perror("VIDIOC_REQBUFS error \n");
//return -1;
6.申请物理内存
将申请到的帧缓冲映射到用户空间,这样就可以直接操作采集到的帧了,而不必去复制。将申请到的帧缓冲全部入队列,以便存放采集到的数据.VIDIOC_QBUF,struct v4l2_buffer
VideoBuffer* buffers = calloc( req.count, sizeof(VideoBuffer) );
printf("sizeof(VideoBuffer) is %d\n",sizeof(VideoBuffer));
struct v4l2_buffer buf;
for (numBufs = 0; numBufs < req.count; numBufs++)
memset( &buf, 0, sizeof(buf) );
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = numBufs;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0)
printf("VIDIOC_QUERYBUF error\n");
//return -1;
printf("buf len is %d\n",sizeof(buf));
//内存映射
buffers[numBufs].length = buf.length;
buffers[numBufs].offset = (size_t) buf.m.offset;
buffers[numBufs].start = mmap (NULL, buf.length,PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
printf("buffers.length = %d,buffers.offset = %d ,buffers.start[0] = %d\n",buffers[numBufs].length,buffers[numBufs].offset,buffers[numBufs].start[0]);
printf("buf2 len is %d\n",sizeof(buffers[numBufs].start));
if (buffers[numBufs].start == MAP_FAILED)
perror("buffers error\n");
//return -1;
if (ioctl (fd, VIDIOC_QBUF, &buf) < 0)
printf("VIDIOC_QBUF error\n");
//return -1;
7. 开始视频的采集。
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl (fd, VIDIOC_STREAMON, &type) < 0)
printf("VIDIOC_STREAMON error\n");
// return -1;
8. 出队列以取得已采集数据的帧缓冲,取得原始采集数据。VIDIOC_DQBUF, 将缓冲重新入队列尾,这样可以循环采集。VIDIOC_QBUF
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0)
perror("VIDIOC_DQBUF failed.\n");
//return -1;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
unsigned char *ptcur = buffers[numBufs].start;
DEBUG("buf.bytesused = %d \n",buf.bytesused);
int i1;
for(i1=0; i1<buf.bytesused; i1++)
if((buffers[numBufs].start[i1] == 0x000000FF) && (buffers[numBufs].start[i1+1] == 0x000000C4))
DEBUG("huffman table finded! \nbuf.bytesused = %d\nFFC4 = %d \n",buf.bytesused,i1);
break;
if(i1 == buf.bytesused)printf("huffman table don't exist! \n");
int i;
for(i=0; i<buf.bytesused; i++)
if((buffers[numBufs].start[i] == 0x000000FF) && (buffers[numBufs].start[i+1] == 0x000000D8)) break;
ptcur++;
DEBUG("i=%d,FF=%02x,D8=%02x\n",i,buffers[numBufs].start[i],buffers[numBufs].start[i+1]);
int imagesize =buf.bytesused - i;
DEBUG("buf.bytesused = %d \n",buf.bytesused);
DEBUG ("imagesize = %d \n",imagesize);
9. 停止视频的采集。VIDIOC_STREAMOFF
10. 关闭视频设备。close(fd); 参考技术A 最简单的就是在linux/drivers/media/platform/xxx/module/sensor/目录下拷贝一份现有的摄像头驱动,修改一些参数,驱动名字之类的,一般不会从头写一个新的驱动。
Linux 下wifi 驱动开发—— USB接口WiFi驱动浅析
转: http://blog.csdn.net/zqixiao_09/article/details/51146149
前面学习了SDIO接口的WiFi驱动,现在我们来学习一下USB接口的WiFi驱动,二者的区别在于接口不同。而USB接口的设备驱动,我们前面也有学习,比如USB摄像头驱动、USB鼠标驱动,同样都符合LinuxUSB驱动结构:
USB设备驱动(字符设备、块设备、网络设备)
|
USB 核心
|
USB主机控制器驱动
不同之处只是在于USB摄像头驱动是字符设备,而我们今天要学习的WiFi驱动是网络设备;当然由我们编写的部分还是USB设备驱动部分,下面进入USB接口WiFi驱动的分析,如何分析呢?我们下面从这几个方面入手:
从硬件层面上看,WIFI设备与CPU通信是通过USB接口的,与其他WIFI设备之间的通信是通过无线射频(RF)。
从软件层面上看,Linux操作系统要管理WIFI设备,那么就要将WIFI设备挂载到USB总线上,通过USB子系统实现管理。而同时为了对接网络,又将WIFI设备封装成一个网络设备。
我们以USB接口的WIFI模块进行分析:
a -- 从USB总线的角度去看,它是USB设备;
b -- 从Linux设备的分类上看,它又是网络设备;
c -- 从WIFI本身的角度去看,它又有自己独特的功能及属性,因此它又是一个私有的设备;
通过上述的分析,我们只要抓住这三条线索深入去分析它的驱动源码,整个WIFI驱动框架就会浮现在你眼前。
一、框架整理
1、USB设备驱动
现在我们先从USB设备开始,要写一个USB设备驱动,那么大致步骤如下:
a -- 需要针对该设备定义一个USB驱动,对应到代码中即定义一个usb_driver结构体变量
代码如下:
- struct usb_driver xxx_usb_wifi_driver;
b -- 填充该设备的usb_driver结构体成员变量
代码如下:
- static struct usb_driver xxx_usb_wifi_driver = {
- .name = "XXX_USB_WIFI",
- .probe = xxx_probe,
- .disconnect = xxx_disconnect,
- .suspend = xxx_suspend,
- .resume = xxx_resume,
- .id_table = xxx_table,
- };
c -- 将该驱动注册到USB子系统
代码如下:
- usb_register(&xxx_usb_wifi_driver);
以上步骤只是一个大致的USB驱动框架流程,而最大和最复杂的工作是填充usb_driver结构体成员变量。以上步骤的主要工作是将USB接口的WIFI设备挂载到USB总线上,以便Linux系统在USB总线上就能够找到该设备。
2、网络设备驱动
接下来是网络设备的线索,网络设备驱动大致步骤如下:
a -- 定义一个net_device结构体变量ndev
代码如下:
- struct net_device *ndev;
b -- 初始化ndev变量并分配内存
代码如下:
- ndev=alloc_etherdev();
c -- 填充ndev -> netdev_ops结构体成员变量
代码如下:
- static const struct net_device_ops xxx_netdev_ops= {
- .ndo_init = xxx_ndev_init,
- .ndo_uninit = xxx _ndev_uninit,
- .ndo_open = netdev_open,
- .ndo_stop = netdev_close,
- .ndo_start_xmit = xxx_xmit_entry,
- .ndo_set_mac_address = xxx_net_set_mac_address,
- .ndo_get_stats = xxx_net_get_stats,
- .ndo_do_ioctl = xxx_ioctl,
- };
d -- 填充ndev->wireless_handlers结构体成员变量,该变量是无线扩展功能
代码如下:
- ndev->wireless_handlers = (struct iw_handler_def *)&xxx_handlers_def;
e -- 将ndev设备注册到网络子系统
代码如下:
- register_netdev(ndev);
3、 WIFI设备本身私有的功能及属性
如自身的配置及初始化、建立与用户空间的交互接口、自身功能的实现等。
a -- 自身的配置及初始化
代码如下:
- xxx_read_chip_info();
- xxx_chip_configure();
- xxx_hal_init();
b -- 主要是在proc和sys文件系统上建立与用户空间的交互接口
代码如下:
- xxx_drv_proc_init();
- xxx_ndev_notifier_register();
c -- 自身功能的实现
WIFI的网络及接入原理,如扫描等。同时由于WIFI在移动设备中,相对功耗比较大,因此,对于功耗、电源管理也会在驱动中体现。
二、USB 设备驱动分析
在分析之前,我们需要理解在整个wifi模块中,USB充当什么角色?它的作用是什么?实质上wifi模块上的数据传输有两端,一端是wifi芯片与wifi芯片之间,通过无线射频(RF)进行数据传输;另一端则是wifi芯片与CPU之间,通过USB进行数据传输。
了解Linux的USB驱动的读者都知道,USB驱动分为两种:一种是USB主机驱动;另一种是USB设备驱动。而我们的USB接口的wifi模块对于CPU(主机)来说,属于USB设备,因此采用USB设备驱动。
有了以上信息之后,我们先让Linux系统识别该USB接口的wifi模块,首先我们在驱动源码中大致添加以下几步工作(前面分析过,这里只看步骤,不看代码):
a -- 定义一个usb_driver结构体变量
b -- 填充该设备的usb_driver结构体成员变量
c -- 将该驱动注册到USB子系统
简单完成以上几步工作,再加上板级文件(arch/mach-xxx.c)对USB设备的支持,Linux的USB子系统几乎可以挂载该wifi模块为USB设备了。但是这并不是我们最终想要的结果。我们还要让Linux系统知道它挂载的USB设备属于无线网络设备,同时能够访问它,利用它实施无线网络的工作。
我们都知道,若要让USB设备真正工作起来,需要对USB设备的4个层次(设备、配置、接口、端点)进行初始化。当然这四个层次并不是一定都要进行初始化,而是根据你的USB设备的功能进行选择的,大致初始化流程如下伪代码:
- static struct dvobj_priv *usb_dvobj_init(struct usb_interface *usb_intf)
- {
- int i;
- u8 val8;
- int status= _FAIL;
- struct dvobj_priv *pdvobjpriv;
- //设备
- struct usb_device *pusbd;
- struct usb_device_descriptor *pdev_desc;
- //配置
- struct usb_host_config *phost_conf;
- struct usb_config_descriptor *pconf_desc;
- //接口
- struct usb_host_interface *phost_iface;
- struct usb_interface_descriptor *piface_desc;
- //端点
- struct usb_host_endpoint *phost_endp;
- struct usb_endpoint_descriptor *pendp_desc;
- //设备的初始化
- pdvobjpriv->pusbintf = usb_intf ;
- pusbd =pdvobjpriv->pusbdev = interface_to_usbdev(usb_intf);
- usb_set_intfdata(usb_intf, pdvobjpriv);
- pdev_desc =&pusbd->descriptor;
- //配置的初始化
- phost_conf =pusbd->actconfig;
- pconf_desc =&phost_conf->desc;
- //接口的初始化
- phost_iface =&usb_intf->altsetting[0];
- piface_desc =&phost_iface->desc;
- //端点的初始化,由于wifi模块属于网络设备,传输批量数据,因此需要初始化为批量端点,端点方向(输入、输出)等。同时,由于wifi驱动功能比较多,需要初始化几个输入输出端点。
- for (i = 0; i <pdvobjpriv->nr_endpoint; i++)
- {
- phost_endp = phost_iface->endpoint +i;
- if (phost_endp)
- {
- pendp_desc =&phost_endp->desc;
- //检查是否为输入端点
- usb_endpoint_is_bulk_in(pendp_desc);
- //检查是否为输出端点
- usb_endpoint_is_bulk_out(pendp_desc);
- }
- }
- usb_get_dev(pusbd);
- }
完成以上的初始化工作之后,接下来我们需要理清一下USB接口的作用,它是wifi芯片内部的固件程序与主机上的Linux系统进行数据通信。USB设备通信不像普通字符设备那样采用I/O内存和I/O端口的访问,而是采用一种称为URB(USB Request Block)的USB请求块,URB在整个USB子系统中,相当于通电设备中的“电波”,USB主机与设备的通信,通过“电波”来传递。下面我们就来编写USB接口的读写操作函数,伪代码如下:
- void xxx_wifi_usb_intf_ops(struct _io_ops *pops)
- {
- //当需要进行简单数据的读取时,采用以下操作
- pops->_read8 = &usb_read8;
- pops->_read16 = &usb_read16;
- pops->_read32 = &usb_read32;
- //当需要进行批量数据的读取时,采用以下操作
- pops->_read_port = &usb_read_port;
- //当需要进行简单数据的写时,采用以下操作
- pops->_write8 = &usb_write8;
- pops->_write16 = &usb_write16;
- pops->_write32 = &usb_write32;
- pops->_writeN = &usb_writeN;
- //当需要进行批量数据的写时,采用以下操作
- pops->_write_port = &usb_write_port;
- //取消读写urb
- pops->_read_port_cancel = &usb_read_port_cancel;
- pops->_write_port_cancel = &usb_write_port_cancel;
- }
在进行批量数据的读写时,如usb_read_port()和usb_write_port()函数,需要完成urb创建、初始化、提交、完成处理这个完整的流程。伪代码如下:
1)批量读操作
- static u32 usb_read_port(struct intf_hdl *pintfhdl, u32 addr, u32 cnt, u8 *rmem)
- {
- int err;
- unsigned intpipe;
- PURB purb =NULL;
- structrecv_buf *precvbuf = (structrecv_buf *)rmem;
- structusb_device *pusbd = pdvobj->pusbdev;
- //创建urb,这里是在其它地方创建完成之后,传递过来
- purb =precvbuf->purb;
- //初始化批量urb
- usb_fill_bulk_urb(purb, pusbd, pipe,
- precvbuf->pbuf,
- MAX_RECVBUF_SZ,
- usb_read_port_complete,
- precvbuf);//contextis precvbuf
- //提交urb
- err =usb_submit_urb(purb, GFP_ATOMIC);
- }
2)批量写操作
- u32 usb_write_port(struct intf_hdl *pintfhdl, u32 addr, u32 cnt, u8 *wmem)
- {
- unsigned int pipe;
- intstatus;
- PURB purb = NULL;
- structxmit_priv *pxmitpriv =&padapter->xmitpriv;
- structxmit_buf *pxmitbuf = (struct xmit_buf *)wmem;
- structxmit_frame *pxmitframe = (struct xmit_frame *)pxmitbuf->priv_data;
- structusb_device *pusbd = pdvobj->pusbdev;
- structpkt_attrib *pattrib = &pxmitframe->attrib;
- //创建urb,这里是在其它地方创建完成之后,传递过来
- purb = pxmitbuf->pxmit_urb[0];
- //初始化批量urb
- usb_fill_bulk_urb(purb, pusbd, pipe,
- pxmitframe->buf_addr,//= pxmitbuf->pbuf
- cnt,
- usb_write_port_complete,
- pxmitbuf);//contextis pxmitbuf
- //提交urb
- status = usb_submit_urb(purb,GFP_ATOMIC);
- return ret;
- }
完成以上批量数据的读写操作之后,大家可能会疑问:这不是一般USB设备驱动的操作流程吗?貌似和wifi没有半毛钱的关系啊!从上面看,确实和wifi没有任何联系,但是以上只是一个铺垫。我们一直强调USB接口在wifi模块中充当什么角色,既然是接口,那么它就是为数据传输而生。所以,和wifi扯上关系的就在于usb_read_port()和usb_write_port()这两个函数。
三、读写函数分析
USB接口在wifi模块中的最重要两个函数是usb_read_port()和usb_write_port()。那它们是怎么和wifi扯上关系的呢?我们可以从以下三个方面去分析:
a -- 首先需要明确wifi模块是USB设备,主控(CPU)端是USB主机;
b -- USB主机若需要对wifi模块进行数据的读写时,就必须经过USB接口;
c -- 既然涉及到数据的读写操作,必然要用相应的读写函数,那么usb_read_port()和usb_write_port()即是它们的读写函数。
我们先从读数据开始进行分析,在分析之前,我们必须了解USB设备驱动的读数据过程。USB读取数据操作流程如下:
a -- 通过usb_alloc_urb()函数创建并分配一个URB,作为传输USB数据的载体;
b -- 创建并分配DMA缓冲区,以DMA方式快速传输数据;
c -- 初始化URB,根据wifi的传输数据量,我们需要初始化为批量URB,相应操作函数为usb_fill_bulk_urb();
d -- 将URB提交到USB核心;
e -- 提交成功后,URB的完成函数将被USB核心调用。
我们知道只有当wifi模块有数据可读时,主控端才能成功地读取数据。那么wifi模块什么时候有数据可读呢?——下面重点来了!wifi模块通过RF端接收到无线网络数据,然后缓存到wifi芯片的RAM中,此时,wifi模块就有数据可读了。
经过上面的分析,我们找到了一条USB接口与wifi模块扯上关系的线索,就是wifi模块的接收数据,会引发USB接口的读数据;
现在,我们转到wifi模块的接收函数中,看看是不是真的这样?
在wifi接收函数初始化中,我们可以看到usb_alloc_urb()创建一个中断URB。伪代码如下:
- int xxxwifi_init_recv(_adapter *padapter)
- {
- struct recv_priv *precvpriv = &padapter->recvpriv;
- int i, res = _SUCCESS;
- struct recv_buf *precvbuf;
- tasklet_init(&precvpriv->recv_tasklet, (void(*)(unsigned long))rtl8188eu_recv_tasklet, (unsigned long)padapter);
- precvpriv->int_in_urb = usb_alloc_urb(0, GFP_KERNEL); //创建一个中断URB
- precvpriv->int_in_buf = rtw_zmalloc(INTERRUPT_MSG_FORMAT_LEN);
- //init recv_buf
- _rtw_init_queue(&precvpriv->free_recv_buf_queue);
- _rtw_init_queue(&precvpriv->recv_buf_pending_queue);
- precvpriv -> pallocated_recv_buf = rtw_zmalloc(NR_RECVBUFF *sizeof(struct recv_buf) + 4);
- precvbuf = (struct recv_buf*)precvpriv->precv_buf;
- for(i=0; i < NR_RECVBUFF ; i++)
- {
- _rtw_init_listhead(&precvbuf->list);
- _rtw_spinlock_init(&precvbuf->recvbuf_lock);
- precvbuf->alloc_sz = MAX_RECVBUF_SZ;
- res = rtw_os_recvbuf_resource_alloc(padapter, precvbuf);
- precvbuf->ref_cnt = 0;
- precvbuf->adapter =padapter;
- precvbuf++;
- }
- precvpriv->free_recv_buf_queue_cnt = NR_RECVBUFF;
- skb_queue_head_init(&precvpriv->rx_skb_queue);
- #ifdef CONFIG_PREALLOC_RECV_SKB
- {
- int i;
- SIZE_PTR tmpaddr=0;
- SIZE_PTR alignment=0;
- struct sk_buff *pskb=NULL;
- skb_queue_head_init(&precvpriv->free_recv_skb_queue);
- for(i=0; i<NR_PREALLOC_RECV_SKB; i++)
- {
- pskb = rtw_skb_alloc(MAX_RECVBUF_SZ + RECVBUFF_ALIGN_SZ);
- if(pskb)
- {
- pskb->dev = padapter->pnetdev;
- tmpaddr = (SIZE_PTR)pskb->data;
- alignment = tmpaddr & (RECVBUFF_ALIGN_SZ-1);
- skb_reserve(pskb, (RECVBUFF_ALIGN_SZ - alignment));
- skb_queue_tail(&precvpriv->free_recv_skb_queue, pskb);
- }
- pskb=NULL;
- }
- }
- #endif
- return res;
- }
在rtw_os_recvbuf_resource_alloc函数中,创建一个批量URB和一个DMA缓冲区。伪代码如下:
- int rtw_os_recvbuf_resource_alloc(_adapter *padapter, struct recv_buf *precvbuf)
- {
- int res=_SUCCESS;
- struct dvobj_priv *pdvobjpriv = adapter_to_dvobj(padapter);
- struct usb_device *pusbd = pdvobjpriv->pusbdev;
- precvbuf->irp_pending = _FALSE;
- precvbuf->purb = usb_alloc_urb(0, GFP_KERNEL); //创建一个批量URB
- precvbuf->pskb = NULL;
- precvbuf->reuse = _FALSE;
- precvbuf->pallocated_buf = precvbuf->pbuf = NULL;
- precvbuf->pdata = precvbuf->phead = precvbuf->ptail = precvbuf->pend = NULL;
- precvbuf->transfer_len = 0;
- precvbuf->len = 0;
- #ifdef CONFIG_USE_USB_BUFFER_ALLOC_RX
- precvbuf->pallocated_buf = rtw_usb_buffer_alloc(pusbd, (size_t)precvbuf->alloc_sz, &precvbuf->dma_transfer_addr); //创建一个DMA缓冲区
- precvbuf->pbuf = precvbuf->pallocated_buf;
- if(precvbuf->pallocated_buf == NULL)
- return _FAIL;
- #endif //CONFIG_USE_USB_BUFFER_ALLOC_RX
- return res;
- }
在usb_read_port()函数中,通过usb_fill_bulk_urb()初始化批量URB,并且提交给USB核心,也即USB读取数据操作流程的第3、4步。在usb_fill_bulk_urb()函数中,初始化URB的完成函数usb_read_port_complete(),只有当URB提交完成后,函数usb_read_port_complete()将被调用。伪代码如下:
- static u32 usb_read_port(struct intf_hdl *pintfhdl, u32 addr, u32 cnt, u8 *rmem)
- {
- struct recv_buf *precvbuf = (struct recv_buf *)rmem;
- _adapter *adapter = pintfhdl->padapter;
- struct dvobj_priv *pdvobj = adapter_to_dvobj(adapter);
- struct pwrctrl_priv *pwrctl = dvobj_to_pwrctl(pdvobj);
- struct recv_priv *precvpriv = &adapter->recvpriv;
- struct usb_device *pusbd = pdvobj->pusbdev;
- rtl8188eu_init_recvbuf(adapter, precvbuf);
- precvpriv->rx_pending_cnt++;
- purb = precvbuf->purb;
- //translate DMA FIFO addr to pipehandle
- pipe = ffaddr2pipehdl(pdvobj, addr);
- usb_fill_bulk_urb(purb, pusbd, pipe,
- precvbuf->pbuf,
- MAX_RECVBUF_SZ,
- usb_read_port_complete,
- precvbuf);//context is precvbuf
- err = usb_submit_urb(purb, GFP_ATOMIC);
- return ret;
- }
通过上面的代码,我们可以得知在wifi模块为接收数据做初始化准备时,分配了URB和DMA缓冲区。而在usb_read_port()函数中初始化URB和提交URB。
以上是关于如何在Linux下开发摄像头驱动的主要内容,如果未能解决你的问题,请参考以下文章
Linux 下wifi 驱动开发—— USB接口WiFi驱动浅析