通行证: 用户 密码 域名空间  下载中心 社区论坛 信息公告 MY小屋
联系我们
设为首页
加入收藏

 

QQ,ASP,PHP,JSP,XML,SQL,.Net,编程 程序 网页图象 建站经验 私服
首页 | 新闻资讯 | 编程开发 | 网页设计 | 图形图象 | 网络媒体 | 网站模板 | 数 据 库 | 投稿
论坛 | 操作系统 | 系统优化 | 网络安全 | 黑客技术 | 硬件学堂 | 硬件报价 | 服 务 器 | 地图
专题 | 应用软件 | 聊天通讯 | Q Q 专栏 | 建站经验 | 在线工具 | 站长Club | 注 册 表 | 旧版
社会 | 游戏娱乐 | 设计欣赏 | 疑难解答 | 社区论坛 | 韩国素材 | 素材图库 | 广告服务 | 服务
当前位置:首页>>操作系统>>Linux>>正文 新版上线![旧版]
注:打开慢时请稍等

Linux下的硬件驱动——USB设备(下)(驱动开发部分)

http://www.iyit.net  日期:2006-5-14 12:13:00  来源:DDVIP   点击:
参加讨论


联想软件设计中心嵌入式研发处系统设计工程师
2003年7月

USB骨架程序(usb-skeleton),是USB驱动程序的基础,通过对它源码的学习和理解,可以使我们迅速地了解USB驱动架构,迅速地开发我们自己的USB硬件的驱动。
前言

在上篇《Linux下的硬件驱动--USB设备(上)(驱动配制部分)》中,我们知道了在Linux下如何去使用一些最常见的USB设备。但对于做系统设计的程序员来说,这是远远不够的,我们还需要具有驱动程序的阅读、修改和开发能力。在此下篇中,就是要通过简单的USB驱动的例子,随您一起进入USB驱动开发的世界。

USB驱动开发

在掌握了USB设备的配置后,对于程序员,我们就可以尝试进行一些简单的USB驱动的修改和开发了。这一段落,我们会讲解一个最基础USB框架的基础上,做两个小的USB驱动的例子。

USB骨架

在Linux kernel源码目录中driver/usb/usb-skeleton.c为我们提供了一个最基础的USB驱动程序。我们称为USB骨架。通过它我们仅需要修改极少的部分,就可以完成一个USB设备的驱动。我们的USB驱动开发也是从她开始的。

那些linux下不支持的USB设备几乎都是生产厂商特定的产品。如果生产厂商在他们的产品中使用自己定义的协议,他们就需要为此设备创建特定的驱动程序。当然我们知道,有些生产厂商公开他们的USB协议,并帮助Linux驱动程序的开发,然而有些生产厂商却根本不公开他们的USB协议。因为每一个不同的协议都会产生一个新的驱动程序,所以就有了这个通用的USB驱动骨架程序, 它是以pci 骨架为模板的。

如果你准备写一个linux驱动程序,首先要熟悉USB协议规范。USB主页上有它的帮助。一些比较典型的驱动可以在上面发现,同时还介绍了USB urbs的概念,而这个是usb驱动程序中最基本的。

Linux USB 驱动程序需要做的第一件事情就是在Linux USB 子系统里注册,并提供一些相关信息,例如这个驱动程序支持那种设备,当被支持的设备从系统插入或拔出时,会有哪些动作。所有这些信息都传送到USB 子系统中,在usb骨架驱动程序中是这样来表示的:


static struct usb_driver skel_driver = {
name: "skeleton",
probe: skel_probe,
disconnect: skel_disconnect,
fops: &skel_fops,
minor: USB_SKEL_MINOR_BASE,
id_table: skel_table,
};



变量name是一个字符串,它对驱动程序进行描述。probe 和disconnect 是函数指针,当设备与在id_table 中变量信息匹配时,此函数被调用。

fops和minor变量是可选的。大多usb驱动程序钩住另外一个驱动系统,例如SCSI,网络或者tty子系统。这些驱动程序在其他驱动系统中注册,同时任何用户空间的交互操作通过那些接口提供,比如我们把SCSI设备驱动作为我们USB驱动所钩住的另外一个驱动系统,那么我们此USB设备的read、write等操作,就相应按SCSI设备的read、write函数进行访问。但是对于扫描仪等驱动程序来说,并没有一个匹配的驱动系统可以使用,那我们就要自己处理与用户空间的read、write等交互函数。Usb子系统提供一种方法去注册一个次设备号和file_operations函数指针,这样就可以与用户空间实现方便地交互。

USB骨架程序的关键几点如下:

USB驱动的注册和注销
Usb驱动程序在注册时会发送一个命令给usb_register,通常在驱动程序的初始化函数里。

当要从系统卸载驱动程序时,需要注销usb子系统。即需要usb_unregister 函数处理:


static void __exit usb_skel_exit(void)
{
/* deregister this driver with the USB subsystem */
usb_deregister(&skel_driver);
}
module_exit(usb_skel_exit);




当usb设备插入时,为了使linux-hotplug(Linux中PCI、USB等设备热插拔支持)系统自动装载驱动程序,你需要创建一个MODULE_DEVICE_TABLE。代码如下(这个模块仅支持某一特定设备):


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

MODULE_DEVICE_TABLE (usb, skel_table);




USB_DEVICE宏利用厂商ID和产品ID为我们提供了一个设备的唯一标识。当系统插入一个ID匹配的USB设备到USB总线时,驱动会在USB core中注册。驱动程序中probe 函数也就会被调用。usb_device 结构指针、接口号和接口ID都会被传递到函数中。


static void * skel_probe(struct usb_device *dev,
unsigned int ifnum, const struct usb_device_id *id)




驱动程序需要确认插入的设备是否可以被接受,如果不接受,或者在初始化的过程中发生任何错误,probe函数返回一个NULL值。否则返回一个含有设备驱动程序状态的指针。通过这个指针,就可以访问所有结构中的回调函数。

在骨架驱动程序里,最后一点是我们要注册devfs。我们创建一个缓冲用来保存那些被发送给usb设备的数据和那些从设备上接受的数据,同时USB urb 被初始化,并且我们在devfs子系统中注册设备,允许devfs用户访问我们的设备。注册过程如下:


/* initialize the devfs node for this device
and register it */
sprintf(name, "skel%d", skel->minor);
skel->devfs = devfs_register
(usb_devfs_handle, name,
DEVFS_FL_DEFAULT, USB_MAJOR,
USB_SKEL_MINOR_BASE + skel->minor,
S_IFCHR | S_IRUSR | S_IWUSR |
S_IRGRP | S_IWGRP | S_IROTH,
&skel_fops, NULL);




如果devfs_register函数失败,不用担心,devfs子系统会将此情况报告给用户。

当然最后,如果设备从usb总线拔掉,设备指针会调用disconnect 函数。驱动程序就需要清除那些被分配了的所有私有数据、关闭urbs,并且从devfs上注销调自己。


/* remove our devfs node */
devfs_unregister(skel->devfs);




现在,skeleton驱动就已经和设备绑定上了,任何用户态程序要操作此设备都可以通过file_operations结构所定义的函数进行了。首先,我们要open此设备。在open函数中MODULE_INC_USE_COUNT 宏是一个关键,它的作用是起到一个计数的作用,有一个用户态程序打开一个设备,计数器就加一,例如,我们以模块方式加入一个驱动,若计数器不为零,就说明仍然有用户程序在使用此驱动,这时候,你就不能通过rmmod命令卸载驱动模块了。



/* increment our usage count for the module */
MOD_INC_USE_COUNT;
++skel->open_count;
/* save our object in the file's private structure */
file->private_data = skel;




当open完设备后,read、write函数就可以收、发数据了。

skel的write、和read函数
他们是完成驱动对读写等操作的响应。

在skel_write中,一个FILL_BULK_URB函数,就完成了urb 系统callbak和我们自己的skel_write_bulk_callback之间的联系。注意skel_write_bulk_callback是中断方式,所以要注意时间不能太久,本程序中它就只是报告一些urb的状态等。

read 函数与write 函数稍有不同在于:程序并没有用urb 将数据从设备传送到驱动程序,而是我们用usb_bulk_msg 函数代替,这个函数能够不需要创建urbs 和操作urb函数的情况下,来发送数据给设备,或者从设备来接收数据。我们调用usb_bulk_msg函数并传提一个存储空间,用来缓冲和放置驱动收到的数据,若没有收到数据,就失败并返回一个错误信息。

usb_bulk_msg函数
当对usb设备进行一次读或者写时,usb_bulk_msg 函数是非常有用的; 然而, 当你需要连续地对设备进行读/写时,建议你建立一个自己的urbs,同时将urbs 提交给usb子系统。

skel_disconnect函数
当我们释放设备文件句柄时,这个函数会被调用。MOD_DEC_USE_COUNT宏会被用到(和MOD_INC_USE_COUNT刚好对应,它减少一个计数器),首先确认当前是否有其它的程序正在访问这个设备,如果是最后一个用户在使用,我们可以关闭任何正在发生的写,操作如下:



/* decrement our usage count for the device */
--skel->open_count;
if (skel->open_count <= 0) {
/* shutdown any bulk writes that might be
going on */
usb_unlink_urb (skel->write_urb);
skel->open_count = 0;
}
/* decrement our usage count for the module */
MOD_DEC_USE_COUNT;



最困难的是,usb 设备可以在任何时间点从系统中取走,即使程序目前正在访问它。usb驱动程序必须要能够很好地处理解决此问题,它需要能够切断任何当前的读写,同时通知用户空间程序:usb设备已经被取走。

如果程序有一个打开的设备句柄,在当前结构里,我们只要把它赋值为空,就像它已经消失了。对于每一次设备读写等其它函数操作,我们都要检查usb_device结构是否存在。如果不存在,就表明设备已经消失,并返回一个-ENODEV错误给用户程序。当最终我们调用release 函数时,在没有文件打开这个设备时,无论usb_device结构是否存在、它都会清空skel_disconnect函数所作工作。

Usb 骨架驱动程序,提供足够的例子来帮助初始人员在最短的时间里开发一个驱动程序。更多信息你可以到linux usb开发新闻组去寻找。

U盘、USB读卡器、MP3、数码相机驱动

对于一款windows下用的很爽的U盘、USB读卡器、MP3或数码相机,可能Linux下却不能支持。怎么办?其实不用伤心,也许经过一点点的工作,你就可以很方便地使用它了。通常是此U盘、USB读卡器、MP3或数码相机在WindowsXP中不需要厂商专门的驱动就可以识别为移动存储设备,这样的设备才能保证成功,其他的就看你的运气了。

USB存储设备,他们的read、write等操作都是通过上章节中提到的钩子,把自己的操作钩到SCSI设备上去的。我们就不需要对其进行具体的数据读写处理了。

第一步:我们通过cat /proc/bus/usb/devices得到当前系统探测到的USB总线上的设备信息。它包括Vendor、ProdID、Product等。下面是我买的一款杂牌CF卡读卡器插入后的信息片断:



T: Bus=01 Lev=01 Prnt=01 Port=01 Cnt=02 Dev#= 5 Spd=12 MxCh= 0
D: Ver= 1.10 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=8 #Cfgs= 1
P: Vendor=07c4 ProdID=a400 Rev= 1.13
S: Manufacturer=USB
S: Product=Mass Storage
C:* #Ifs= 1 Cfg#= 1 Atr=80 MxPwr=70mA
I: If#= 0 Alt= 0 #EPs= 2 Cls=08(vend.) Sub=06 Prot=50 Driver=usb-storage
E: Ad=81(I) Atr=02(Bulk) MxPS= 64 Ivl= 0ms
E: Ad=02(O) Atr=02(Bulk) MxPS= 64 Ivl= 0ms




其中,我们最关心的是Vendor=07c4 ProdID=a400和Manufacturer=USB(果然是杂牌,厂商名都看不到)Product= Mass Storage。

对于这些移动存储设备,我们知道Linux下都是通过usb-storage.o驱动模拟成scsi设备去支持的,之所以不支持,通常是usb-storage驱动未包括此厂商识别和产品识别信息(在类似skel_probe的USB最初探测时被屏蔽了)。对于USB存储设备的硬件访问部分,通常是一致的。所以我们要支持它,仅需要修改usb-storage中关于厂商识别和产品识别列表部分。

第二部,打开drivers/usb/storage/unusual_devs.h文件,我们可以看到所有已知的产品登记表,都是以UNUSUAL_DEV(idVendor, idProduct, bcdDeviceMin, bcdDeviceMax, vendor_name, product_name, use_protocol, use_transport, init_function, Flags)方式登记的。其中相应的涵义,你就可以根据命名来判断了。所以只要我们如下填入我们自己的注册,就可以让usb-storage驱动去认识和发现它。


UNUSUAL_DEV(07c4, a400, 0x0000, 0xffff,
" USB ", " Mass Storage ",
US_SC_SCSI, US_PR_BULK, NULL,
US_FL_FIX_INQUIRY | US_FL_START_STOP |US_FL_MODE_XLATE )




注意:添加以上几句的位置,一定要正确。比较发现,usb-storage驱动对所有注册都是按idVendor, idProduct数值从小到大排列的。我们也要放在相应位置。

最后,填入以上信息,我们就可以重新编译生成内核或usb-storage.o模块。这时候插入我们的设备就可以跟其他U盘一样作为SCSI设备去访问了。

键盘飞梭支持

目前很多键盘都有飞梭和手写板,下面我们就尝试为一款键盘飞梭加入一个驱动。在通常情况,当我们插入USB接口键盘时,在/proc/bus/usb/devices会看到多个USB设备。比如:你的USB键盘上的飞梭会是一个,你的手写板会是一个,若是你的USB键盘有USB扩展连接埠,也会看到。

下面是具体看到的信息


T: Bus=02 Lev=00 Prnt=00 Port=00 Cnt=00 Dev#= 1 Spd=12 MxCh= 2
B: Alloc= 11/900 us ( 1%), #Int= 1, #Iso= 0
D: Ver= 1.00 Cls=09(hub ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1
P: Vendor=0000 ProdID=0000 Rev= 0.00
S: Product=USB UHCI Root Hub
S: SerialNumber=d800
C:* #Ifs= 1 Cfg#= 1 Atr=40 MxPwr= 0mA
I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub
E: Ad=81(I) Atr=03(Int.) MxPS= 8 Ivl=255ms
T: Bus=02 Lev=01 Prnt=01 Port=01 Cnt=01 Dev#= 3 Spd=12 MxCh= 3
D: Ver= 1.10 Cls=09(hub ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1
P: Vendor=07e4 ProdID=9473 Rev= 0.02
S: Manufacturer=ALCOR
S: Product=Movado USB Keyboard
C:* #Ifs= 1 Cfg#= 1 Atr=e0 MxPwr=100mA
I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub
E: Ad=81(I) Atr=03(Int.) MxPS= 1 Ivl=255ms



找到相应的信息后就可开始工作了。实际上,飞梭的定义和键盘键码通常是一样的,所以我们参照drivers/usb/usbkbd..c代码进行一些改动就可以了。因为没能拿到相应的硬件USB协议,我无从知道飞梭在按下时通讯协议众到底发什么,我只能把它的信息打出来进行分析。幸好,它比较简单,在下面代码的usb_kbd_irq函数中if(kbd->new[0] == (char)0x01)和if(((kbd->new[1]>>4)&0x0f)!=0x7)就是判断飞梭左旋。usb_kbd_irq函数就是键盘中断响应函数。他的挂接,就是在usb_kbd_probe函数中
FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp,
usb_kbd_irq, kbd, endpoint->bInterval);



一句中实现。

从usb骨架中我们知道,usb_kbd_probe函数就是在USB设备被系统发现是运行的。其他部分就都不是关键了。你可以根据具体的探测值(Vendor=07e4 ProdID=9473等)进行一些修改就可以了。值得一提的是,在键盘中断中,我们的做法是收到USB飞梭消息后,把它模拟成左方向键和右方向键,在这里,就看你想怎么去响应它了。当然你也可以响应模拟成F14、F15等扩展键码。

在了解了此基本的驱动后,对于一个你已经拿到通讯协议的键盘所带手写板,你就应该能进行相应驱动的开发了吧。

程序见附录1:键盘飞梭驱动。

使用此驱动要注意的问题:在加载此驱动时你必须先把hid设备卸载,加载完usbhkey.o模块后再加载hid.o。因为若hid存在,它的probe会屏蔽系统去利用我们的驱动发现我们的设备。其实,飞梭本来就是一个hid设备,正确的方法,或许你应该修改hid的probe函数,然后把我们的驱动融入其中。

参考资料


《LINUX设备驱动程序》
ALESSANDRO RUBINI著
LISOLEG 译

《Linux系统分析与高级编程技术》
周巍松 编著

Linux Kernel-2.4.20源码和文档说明


附录1:键盘飞梭驱动


#include
#include
#include
#include
#include
#include
#include

/*
* Version Information
*/
#define DRIVER_VERSION ""
#define DRIVER_AUTHOR "TGE HOTKEY "
#define DRIVER_DESC "USB HID Tge hotkey driver"

#define USB_HOTKEY_VENDOR_ID 0x07e4
#define USB_HOTKEY_PRODUCT_ID 0x9473
//厂商和产品ID信息就是/proc/bus/usb/devices中看到的值

MODULE_AUTHOR( DRIVER_AUTHOR );
MODULE_DESCRIPTION( DRIVER_DESC );

struct usb_kbd {
struct input_dev dev;
struct usb_device *usbdev;
unsigned char new[8];
unsigned char old[8];
struct urb irq, led;
// devrequest dr;
//这一行和下一行的区别在于kernel2.4.20版本对usb_kbd键盘结构定义发生了变化
struct usb_ctrlrequest dr;
unsigned char leds, newleds;
char name[128];
int open;
};
//此结构来自内核中drivers/usb/usbkbd..c

static void usb_kbd_irq(struct urb *urb)
{
struct usb_kbd *kbd = urb->context;
int *new;
new = (int *) kbd->new;

if(kbd->new[0] == (char)0x01)
{
if(((kbd->new[1]>>4)&0x0f)!=0x7)
{
handle_scancode(0xe0,1);
handle_scancode(0x4b,1);
handle_scancode(0xe0,0);
handle_scancode(0x4b,0);
}
else
{
handle_scancode(0xe0,1);
handle_scancode(0x4d,1);
handle_scancode(0xe0,0);
handle_scancode(0x4d,0);
}
}


printk("new=%x %x %x %x %x %x %x %x",
kbd->new[0],kbd->new[1],kbd->new[2],kbd->new[3],
kbd->new[4],kbd->new[5],kbd->new[6],kbd->new[7]);

}

static void *usb_kbd_probe(struct usb_device *dev, unsigned int ifnum,
const struct usb_device_id *id)
{
struct usb_interface *iface;
struct usb_interface_descriptor *interface;
struct usb_endpoint_descriptor *endpoint;
struct usb_kbd *kbd;
int pipe, maxp;

iface = &dev->actconfig->interface[ifnum];
interface = &iface->altsetting[iface->act_altsetting];

if ((dev->descriptor.idVendor != USB_HOTKEY_VENDOR_ID) ||
(dev->descriptor.idProduct != USB_HOTKEY_PRODUCT_ID) ||
(ifnum != 1))
{
return NULL;
}
if (dev->actconfig->bNumInterfaces != 2)
{
return NULL;
}

if (interface->bNumEndpoints != 1) return NULL;

endpoint = interface->endpoint + 0;

pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe));

usb_set_protocol(dev, interface->bInterfaceNumber, 0);
usb_set_idle(dev, interface->bInterfaceNumber, 0, 0);

printk(KERN_INFO "GUO: Vid = %.4x, Pid = %.4x, Device = %.2x, ifnum = %.2x, bufCount = %.8x\\n",
dev->descriptor.idVendor,dev->descriptor.idProduct,dev->descriptor.bcdDevice, ifnum, maxp);

if (!(kbd = kmalloc(sizeof(struct usb_kbd), GFP_KERNEL))) return NULL;
memset(kbd, 0, sizeof(struct usb_kbd));

kbd->usbdev = dev;

FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp,
usb_kbd_irq, kbd, endpoint->bInterval);

kbd->irq.dev = kbd->usbdev;

if (dev->descriptor.iManufacturer)
usb_string(dev, dev->descriptor.iManufacturer, kbd->name, 63);

if (usb_submit_urb(&kbd->irq)) {
kfree(kbd);
return NULL;
}

printk(KERN_INFO "input%d: %s on usb%d:%d.%d\\n",
kbd->dev.number, kbd->name, dev->bus->busnum, dev->devnum, ifnum);

return kbd;
}

static void usb_kbd_disconnect(struct usb_device *dev, void *ptr)
{
struct usb_kbd *kbd = ptr;
usb_unlink_urb(&kbd->irq);
kfree(kbd);

}

static struct usb_device_id usb_kbd_id_table [] = {
{ USB_DEVICE(USB_HOTKEY_VENDOR_ID, USB_HOTKEY_PRODUCT_ID) },
{ } /* Terminating entry */
};

MODULE_DEVICE_TABLE (usb, usb_kbd_id_table);

static struct usb_driver usb_kbd_driver = {
name: "Hotkey",
probe: usb_kbd_probe,
disconnect: usb_kbd_disconnect,
id_table: usb_kbd_id_table,
NULL,
};

static int __init usb_kbd_init(void)
{
usb_register(&usb_kbd_driver);
info(DRIVER_VERSION ":" DRIVER_DESC);
return 0;
}

static void __exit usb_kbd_exit(void)
{
usb_deregister(&usb_kbd_driver);
}

module_init(usb_kbd_init);
module_exit(usb_kbd_exit);

关于作者

赵明,联想软件设计中心嵌入式研发处系统设计工程师,一直致力于WinCE、WinXPE、Linux等嵌入式系统研究。您可以通过carl__zhao@163.com与他联系。


编辑:黑鹰 [发送给好友] [打印本页] [关闭窗口] [返回顶部]
上一篇:SCO, GNU and Linux
下一篇:Linux下的硬件驱动——USB设备(上)(驱动配置部分)
转载请注明来源:www.iyit.net
特别声明: 本站除部分特别声明禁止转载的专稿外的其他文章可以自由转载,但请务必注明出处和原始作者。文章版权归文章原始作者所有。对于被本站转载文章的个人和网站,我们表示深深的谢意。如果本站转载的文章有版权问题请联系编辑人员,我们尽快予以更正。

 相关文章
深入理解硬盘的 Linux 分区 《Linux内核完全注释》V1.9.5版 Linux的进程查看
使用图形客户端远程访问Linux服务器 LINUX新手入门及安装配置faq200(下) Linux下安装和使用杀毒软件AntiVir
在Linux下使用金山词霸2003 在Linux下安装和使用MySQL Linux 探索 第一幕 传奇的开始
Linux下的游戏 Linux常用命令 解析Fedora最新桌面Linux系统
100个最佳Linux站点 分享经验丰富的 Linux程序员 Spence Mu 发布《linux内核0.11完全注释》修正版1
Linux的引导过程剖析 Linux日志式文件系统面面观 Linux常用操作疑难解答(2)
Linux常用操作疑难解答(3) 制作Linux启动盘的四种方法 Grub轻松解决单硬盘3OS+n个Linux多系统
安装:SuSE Linux FTP版安装指南 Linux操作系统介绍 发布《linux内核0.11(0.95)完全注释》修
最新更新 热点排行 推荐新闻
忘了MySQL的管理员密码怎么办
忘了root的密码怎么办?
怎样显示一个字呢
深入理解硬盘的 Linux 分区
使用GRUB引导多个操作系统
忘了MySQL的管理员密码怎么办
忘了root的密码怎么办?
怎样显示一个字呢
深入理解硬盘的 Linux 分区
使用GRUB引导多个操作系统
《Linux内核完全注释》V1.9.5版
什么是 screen
分区大小调整完全手册
Framebuffer HOWTO英文
FRramebuffer HOWTO英文(续)
忘了MySQL的管理员密码怎么办
忘了root的密码怎么办?
怎样显示一个字呢
深入理解硬盘的 Linux 分区
使用GRUB引导多个操作系统
优秀公益广告作品欣赏(8)
新开放QQ免费挂级网站
java数据类型转换
免费在QQ上看在线电影电视听音乐
QQ珊瑚虫外挂4.0版本发布!
免费把QQ炫铃设为本机QQ的系统提示音
Windows XP专业版IIS连接数的更改
优秀公益广告作品欣赏(7)
WEB服务器配置全攻略(三)
腾讯QQ调整升级条件不再诱发网民“通宵
ASP.NET 2.0 中的异步页功能应用
硬盘坏道修复及数据恢复宝典
免费登录搜索引擎入口大全
搜索引擎注册九大秘法
小心摄像头成为黑客偷窥你的眼睛
内存混插常见问题和解决方法
Office2007简体中文版浮出水面 美图抢
0689版Windows Live Messenger五大看点
比旧版看变化 QQ2006Beta2很不错
给MSN Messenger好友列表减肥
 友情链接
设置首 页 - 版权声明 - 广告服务 - 关于我们 - 联系我们 - 友情连接
Copyrights © 2004-2006 iYiT.Net All Rights Reserved.
网站合作、广告联系QQ:147007642、466949678
易特网络技术 点击这里给我发消息