Chinaunix首页 | 论坛 | 博客
  • 博客访问: 822481
  • 博文数量: 124
  • 博客积分: 1927
  • 博客等级: 上尉
  • 技术积分: 932
  • 用 户 组: 普通用户
  • 注册时间: 2010-08-31 14:06
文章分类

全部博文(124)

文章存档

2018年(5)

2017年(2)

2016年(6)

2015年(4)

2014年(24)

2013年(7)

2012年(11)

2011年(13)

2010年(52)

我的朋友

分类: LINUX

2010-10-15 15:35:06

本文主要介绍在uClinux下,通过加载模块的方式调试IO控制蜂鸣器的驱动程序。实验过程与上篇文章所讲的过程基本相似,更多注重细节及注意事项。

        本文适合学习ARM—Linux的初学者。

//==================================================================

硬件平台:MagicARM2200教学试验开发平台(LPC2290)

Linux version 2.4.24,gcc version 2.95.3

电路连接:P0.7——蜂鸣器,低电平发声。

实验条件:uClinux内核已经下载到开发板上,能够正常运行;与宿主机相连的网络、串口连接正常。

//==================================================================

        编写蜂鸣器的驱动程序相对来说容易实现,不需要处理中断等繁琐的过程,本文以蜂鸣器的驱动程序为例,详细说明模块化驱动程序设计的主要过程和注意事项。

        一、编写驱动程序

        驱动程序的编写与上文所说的编写过程基本相同,这里再详细说明一下。

//==========================================

//蜂鸣器驱动程序:beep.c文件

//-------------------------------------------------------------------

#include     /*模块相关*/
#include       /*内核相关*/
#include        /*linux定义类型*/
#include              /*文件系统 file_opertions 结构体定义*/
#include        /*出错信息*/

/*PINSEL0 注意:低2位是UART0复用口,不要改动*/

#define PINSEL0 (*((volatile unsigned*) 0xE002C000))   

/*P0口控制寄存器*/

#define IO0PIN (*((volatile unsigned*) 0xE0028000))
#define IO0SET (*((volatile unsigned*) 0xE0028004))
#define IO0DIR (*((volatile unsigned*) 0xE0028008))
#define IO0CLR (*((volatile unsigned*) 0xE002800C))

#define MAJOR_NUMBER 254    /*自定义的主设备号*/
#define BEEP_CMD 0                  /*自定义的控制命令*/

/*函数声明*/

static int beep_open(struct inode *inode, struct file *file);
static int beep_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg);
static int beep_release(struct inode *inode, struct file *file);

static int beep_init(void);
static void beep_cleanup(void);
/********************************************************/

volatile static int beep_major = MAJOR_NUMBER;      /*全局变量:主设备号自定义为254*/
/********************************************************/

/*注册函数:

用到file_operations结构体。将蜂鸣器结构体自命名为 beep_test ,在注册模块时要用到

*/
static struct file_operations beep_test =
{
    owner : THIS_MODULE,

    ioctl : beep_ioctl,
   open : beep_open,
   release : beep_release,
}; /*注意:此处的分号(;)不要丢掉*/


/*********************************************************/
#define BEEPCON 0x00000080
static void beep_port_init(void) //蜂鸣器端口初始化:设置P0.7口为输出,初始值为高(蜂鸣器不发声)
{
    IO0DIR = BEEPCON;
    IO0SET = BEEPCON;
}

static void beep(int beep_status) //蜂鸣器操作:根据参数(beep_status)状态判断是否发声
{
    if(beep_status == 0)
        IO0CLR = BEEPCON;
    else
        IO0SET = BEEPCON;
}

static int beep_open(struct inode *inode, struct file *file) //beep_test结构体中的open()函数实体,以下同
{
    MOD_INC_USE_COUNT; //注册模块数加1
    beep_port_init();

   return 0;
}

static int beep_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
    if(cmd == 0)
    {
        printk("beep on!\n");
   beep(0);
    }
else
{
        printk("beep off!\n");
   beep(1);
}

return 0;
}

static int beep_release(struct inode *inode, struct file *file)
{
    MOD_DEC_USE_COUNT; //模块数减1
    return 0;
}

static int beep_init(void) //模块加载、初始化函数:将模块加载到内核运行
{
    int result;

     result = register_chrdev(beep_major, "named_beep", &beep_test);

   if(result < 0)
   {
         printk(KERN_INFO"beep: can't get major number\n");
       return result;
   }
   if(beep_major == 0) beep_major = result;
   printk(KERN_INFO"beep: init OK!\n");

   /*注意:驱动程序运行在内核空间,从内核打印信息要用printk()函数而不是printf()函数,而且要配有优先级*/

   return 0;
}

static void beep_cleanup(void) //模块卸载函数:将模块从内核卸载出去
{
    unregister_chrdev(beep_major, "named_beep");
}

/************************************************************/

/*以下部分是驱动程序的关键,后面做详细说明*/

//module_init(beep_init);
//module_exit(beep_cleanup);


int init_module(void) //加载模块
{
    return beep_init();
}

void cleanup_module(void) //卸载模块
{
    beep_cleanup();
}

//-------------------------------------------------------------------

//驱动程序文件结束

//==========================================

        以上是整个驱动程序文件的全部内容,将文件保存,这里将其命名为beep.c。整个驱动程序很简单,只填写了几个操作函数 beep_open()、beep_release()和beep_ioctl()。其实控制蜂鸣器用beep_ioctl()一个函数即可,其它函数基本都是空操作。

        在驱动文件最后的两个函数对驱动程序来数是及其重要的。应用程序与内核的区别就是应用程序从头到尾完成一个任务,而内核则为以后处理某些请求而注册自己,完成这个任务后,他的“主”函数就立即终止。换句话说,init_module()函数(名称不能更改)是模块入口点,如同应用程序的main()函数一样,换句话说,模块入口点init_module()函数的任务就是为以后调用模块的函数做准备;cleanup_module()函数(名称不能更改)是模块的第二个入口点,此函数仅当模块被卸载前才被调用。它的功能是去掉init_module()函数所作的事情。这两个函数由头文件声明,有关模块实现的源代码可以参见../kernel/module.c。

        init_module()函数在模块被加载时执行,模块的初始化就是通过调用init_module()函数完成的。它注册驱动设备,需调用register_chrdev()函数实现。register_chrdev有3个参数:(1):希望获得的设备主号,即beep_major全局变量,如果是0,系统将选择一个没有被占用的设备号返回;(2):设备文件名,自定义设备文件名,这里用named_beep,它返回这个驱动程序所使用的主设备号;(3):用来登记驱动程序实际执行操作的函数指针,即beep_test结构体。如果登记成功,register_chrdev返回设备的主设备号;否则返回一个负值。

        模块是内核的一部分,但并未被编辑到内核中,他们被分别编译和连接成目标文件。用命令insmod插入一个模块到内核中,用命令rmmod卸载一个模块。这两个命令分别调用init_module()函数和cleanup_module()函数。关于insmod和rmmod命令,后面还会用到。

        在2.3版本以后的Linux内核中,提供了一种新的方法来命名这两个函数。例如可以定义beep_init()和beep_cleanup()两个函数,然后在源代码文件末尾使用下面的语句,其效果是一样的。

module_init(beep_init);

module_exit(beep_cleanup);

        注意:这两个宏是在中被定义的,所以源码文件中必须包括这个头文件。而且,这两行语句必须在函数声明后使用,否则会编译出错。

        驱动程序部分先暂且介绍到这,继续往下介绍,如何将驱动程序加载到内核中去,又如何利用驱动程序来控制蜂鸣器发声。

        二、编译驱动程序

        编译驱动程序的过程比较无聊,按照步骤一步一步进行即可。但首先需要了解linux的基本操作命令,最好会Makefile文件的编写,至少能看懂也行。

        下面一步一步的介绍:

        1、将驱动程序beep.c文件传到宿主机中。

        这里所说的“宿主机”是运行Linux的PC机,可以是安装了Linux操作系统的本地机,亦可以是Linux服务器。由于嵌入式Linux的开发板资源有限,不可能在开发板上运行开发和调试工具。统称需要交叉编译调试的方式进行,即“宿主机+目标板”的形式。程序在宿主机上编译—连接—定位,得到的可执行文件则在目标板上运行。而“目标板”就是实验的硬件平台MagicARM2200教学试验开发平台。

        本实验是在PC机上通过虚拟机搭建的宿主机。目标板和宿主机通过串口和网口连接,其中串口当作终端,作为人机交互界面。若目标板可以看成一台计算机的话,那么串口终端就相当于这台计算机的显示器,通过linux命令对目标板进行相关操作;而网口是与宿主机相连接,作为数据传输、共享的通道。这里利用linux的NFS服务器将宿主机系统下/home/armwork目录作为共享目录,在目标板上通过mount命令将此目录挂在到目标板上的/mnt目录下,于是打开目标板的/mnt目录所见的内容就是宿主机上/home/armwork目录的内容。

         当然以上所说的内容包括宿主机的建立、交叉开发环境(arm-elf-gcc)的安装、uClinux系统移植、网卡串口驱动、嵌入TCP/IP协议栈等工作都已经做好,这里只是利用这一平台介绍驱动程序的编写。而且这些工作步骤也比较单调,可以很容易找到现成的步骤说明,这里就不做过多说明了。

         说了一堆前提条件,现在开始继续。下面的工作都是在宿主机上进行的。

        前面所说的beep.c驱动文件是在Windows环境下编写的(当然也可以在Linux下编写,如用vi编写),使用SSH Secure或FlashFXP等FTP工具,将beep.c文件上传到宿主机中,先在宿主机的/home/armwork目录下建立一个新目录命名为beep。在宿主机的终端命令行上输入下面两条命令:(这里假定宿主机中已经存在有/home/armwork目录,当然也可以直接在图形编辑环境下新建目录)

# cd /home/armwork

# mkdir beep

# cd beep

这三条命令意思分别为:将/home/armwork目录指定为当前目录;在当前目录下建立beep目录;将beep目录指定为当前目录。

        然后,利用FTP工具将beep.c文件上传到刚刚建立的beep目录下,输入 ls 命令显示当前目录下的内容:

# ls -l

(命令均为为字母l,不是数字1)显示内容为文件的详细信息:

-rw-r--r--    1 root     root         2891 5月 5 18:12 beep.c

依次为操作权限、用户、文件大小、日期、文件名等信息。

        2、编写Makefile文件。

        以下是Makefile文件的详细内容,将其保存命名为Makefile(文件名不能更改)

#---------------------------------------------------------------------------------#

#各项对应驱动程序的文件名。

EXEC = beep
OBJS = beep.o
SRC = beep.c

#交叉环境所在的目录,根据各自机器存放位置修改。

INCLUDE = /usr/src/uClinux-dist/linux-2.4.x/include

#所使用的交叉环境
CC = arm-elf-gcc
LD = arm-elf-ld


MODCFLAGS = -D__KERNEL__ -I$(INCLUDE) -Wall -O2 -fno-strict-aliasing -fno-common -pipe -fno-builtin -D__linux__ -g -DNO_MM -mapcs-32 -march=armv4 -mtune=arm7tdmi -mshort-load-bytes -msoft-float -nostdinc -iwithprefix include
LDFLAGS = -m armelf -r

all: $(EXEC)

$(EXEC): $(OBJS)
   $(LD) $(LDFLAGS) -o $@ $(OBJS)

%.o:%.c
    $(CC) $(MODCFLAGS) -mapcs -c $< -o $@

clean:
     -rm -f $(EXEC) *.elf *.gdb *.o

#---------------------------------------------------------------------------------#

        其中代码的含义这里不做详细讲解,其作用就是将beep.c文件编译生成beep.o文件和beep可执行文件。关于Makefile文件的编写,可以参考相关资料,这里不对写法做详细说明。

        3、编译驱动程序。

        用同样的方法,将Makefile文件上传到宿主机的beep目录下。可以利用 ls 命令查看目录内容。确定文件正确并传输成功,在beep目录下输入make命令编译。

# make

显示如下结果:

arm-elf-gcc -D__KERNEL__ -I/usr/src/uClinux-dist/linux-2.4.x/include -Wall -O2 -fno-strict-aliasing -fno-common -pipe -fno-builtin -D__linux__ -g -DNO_MM -mapcs-32 -march=armv4 -mtune=arm7tdmi -mshort-load-bytes -msoft-float -nostdinc -iwithprefix include -mapcs -c beep.c -o beep.o
arm-elf-ld -m armelf -r -o beep beep.o

        make命令是在当前目录下找到Makefile文件,并对Makefile文件的代码进行解析、执行。而Makefile文件就类似于DOS下的批处理文件。如果遇到问题请查看Makefile文件、操作权限等是否正确。

        如果一切顺利,那么驱动程序就编译成功了,beep目录下会多出几个文件,用 ls 命令查看:

# ls -l

显示如下:

-rw-r--r--    1 root     root        86762 5月 6 09:35 beep
-rw-r--r--    1 root     root         2891 5月 5 18:12 beep.c
-rw-r--r--    1 root     root        86624 5月 6 09:35 beep.o
-rw-r--r--    1 root     root          547 5月 5 14:35 Makefile

        其中beep文件即为可执行文件。后面介绍要加载到模块的文件就是此文件。

        三、加载模块到内核

        前面已经把驱动程序beep.c编译成可执行文件beep,那么这个beep可执行文件即为要加载的所谓的“模块”。既然模块已经做好,下面的工作就轻松了,加载工作非常简单。加载模块的工作是在目标板上进行的,因为我们所要做的就是为目标板做驱动程序,所要加载的模块是要加载到目标板上的uClinux内核中。宿主机与目标板上都运行着linux系统,而其用途是不同的,这一点一定要注意,不要混淆。这里再强调一遍:宿主机的Linux系统安装有编译调试工具,为的是编译C语言程序,生成目标文件(.o文件)和可执行文件。而目标板上的uClinux系统是为实际应用所做的系统,它仅仅是为了直接运行宿主机所生成的可执行代码。这样做的目的就是为了节省目标板的硬件开销,或者说是为了完成在目标板上不可能完成的工作,即编译调试程序。

        首先运行目标板,成功运行uClinux系统,并成功把宿主机上的/home/armwork目录挂在到目标板的/mnt目录下。这一步的作用前面已经说过,是为了让目标板共享宿主机上的/home/armwork目录,即要想打开宿主机上的/home/armwork目录,只要打开目标板上的/mnt目录就可以了。于是前面所编译生成的beep可执行文件就可以直接通过这个目录获取了。

        以下操作都是对目标板进行的,这就需要通过串口的人机终端进行操作。首先连接好串口数据线。若在Windows下,则打开超级终端(开始->所有程序->附件->通讯->超级终端),新建一个超级终端并设置参数使之与目标板相匹配。若在宿主机Linux下,则启动minicom(调整好终端命令行的大小后,输入 minicom命令)根据minicom的提示,按CTRL+A,松开后再按Z,进入minicom配置界面,同样要配置成与目标板相匹配的参数。以下操作都是在超级终端或minicom下进行输入的。

        接下来,在目标板上建立设备节点,输入如下命令:

> cd /mnt/beep

> rm -f /dev/beep

> mknod /dev/beep c 254 0

这两行命令分别为:设置/mnt/beep目录为当前目录;强行删除原有相同名称的设备节点;创建名为beep的设备节点,类型为字符型设备,主设备号为254,从设备号为0。

因为/mnt目录已经被挂在到宿主机的/home/armwork目录了,那么查看/home/armwork目录下的文件就可以通过查看/mnt目下的文件方式实现了。

        从命令中能够看出,设备节点是存储在/dev目录下的,可以通过下面命令查看系统已经建立了哪些设备节点。

> ls /dev

        接下来,加载模块到内核,使用insmod命令

> insmod beep

这一命令将当前目录(/mnt/beep目录)下的beep刻执行文件加载到内核中,执行insmod命令,即调用init_modele()函数,显示结果如下:

Using beep
beep: init OK!

第一句是系统提示的输出,后一句是在驱动程序的init_modele()函数中执行beep_init()函数实现的输出,可以根据需要修改beep_init()函数。

        以上工作需要多次输入命令,操作繁琐,容易出错。要解决这一问题可以将以上输入的命令编写到一起,保存为一个文本文件,如下:

#!/bin/sh

rm -f /dev/beep
mknod /dev/beep c 254 0

insmod beep

将以上代码保存自命名为loadbeep文件,要加载模块的时候只需执行loadbeep文件即可。

> ./loadbeep

注意:是“.”和“/”后面跟文件名,表示执行此文件。执行此文件等同于执行以上命令。同时还用注意权限问题,如果出现“./loadbeep: Permission denied ”提示信息,表示目标板没有对宿主机文件执行的权限。这时需要在宿主机上修改loadbeep文件的使用权限,在宿主机的root用户(linux下拥有最好权限的用户)下输入以下命令:

# chmod 755 loadbeep

这样,其它用户就拥有执行该文件的权限了。其中命令前的“#”表示root用户,“$”表示其它用户。

        若模块使用后,不再需要,则可以使用rmmod命令卸载模块:

> rmmod beep

这一命令将调用cleanup_module()函数。

        若要查看当前已经加载过的设备模块,可以使用lsmod命令查看:

> lsmod

这一命令只是输出的是/proc目录下的modules文件,当然可以用cat命令直接查看该文件。

        至此,模块加载工作也完成了。

        四、编写应用程序,测试驱动模块

        编写测试应用程序和编写驱动程序的过程基本相同,这里不再重复,程序代码如下:

//==========================================

//测试程序:main.c文件

//-------------------------------------------------------------------

#include

main()
{
    int fd;
    int i;

    fd = open("/dev/beep", O_RDONLY);

    /*打开设备文件beep,O_RDONLY表示以只读方式打开,并会调用驱动程序中的beep_open()函数*/

    if(fd == -1) //若打开失败,则报告出错,退出
    {
        printf("Can not open file\n");
        exit(-1);
    }
   
    for(i=0; i<3; i++) //让蜂鸣器每隔1秒响一次,共响三次
    {
        ioctl(fd, 0, 0); //IO操作函数,指令码为0,调用驱动程序中的beep_ioctl()函数,控制蜂鸣器发声
        sleep(1); //Linux系统函数,让进程暂停一段时间,可用于延时,时间单位是秒
        ioctl(fd, 1, 0); //IO操作函数,指令码为1,调用驱动程序中的beep_ioctl()函数,控制蜂鸣器停止发声
        sleep(1);
    }

    close(fd); //关闭设备文件,并会调用驱动程序中的beep_release()函数

    printf("Success!\n"); //用户程序运行在用户空间,打印信息用printf()函数
     
    return(0);

}

//-------------------------------------------------------------------

//测试程序文件结束

//==========================================

        测试程序完成,下面将测试程序按照上传驱动程序的方法上传到宿主机中。

         在宿主机的/home/armwork目录下新建一个目录,这里命名为exc目录。将测试程序main.c上传到这个目录下。下面编写编译测试程序的Makefile文件。

#---------------------------------------------------------------------------------#

EXEC = main
OBJS = main.o
SRC = main.c

CC =arm-elf-gcc

BASEPATH =/usr/src/uClinux-dist
LIBPATH =$(BASEPATH)/lib
LLIBPATH =$(LIBPATH)/uClibc/lib
INCLUDEPATH =$(BASEPATH)/linux-2.4.x/include

LDFLAGS =-Os -g -Dlinux -D__linux__ -Dunix -D__uClinux__ -DEMBED
LDLIBS =-I$(LIBPATH)/uClibc/include -I$(LIBPATH)/libm -I$(LIBPATH)/libcrypt_old -I$(BASEPATH) -fno-builtin -nostartfiles -D__PIC__ -fpic -msingle-pic-base -I$(INCLUDEPATH)
LDLIBS_EXEC =-Wl,-elf2flt $(LLIBPATH)/crt0.o $(LLIBPATH)/crti.o $(LLIBPATH)/crtn.o -L$(LIBPATH)/uClibc/. -L$(LLIBPATH) -L$(LIBPATH)/libm -L$(LIBPATH)/libnet -L$(LIBPATH)/libdes -L$(LIBPATH)/libaes -L$(LIBPATH)/libpcap -L$(LIBPATH)/libcrypt_old -L$(LIBPATH)/libssl -L$(LIBPATH)/zlib -lc
LDLIBS_OBJS =-c

all: $(EXEC)

$(EXEC): $(OBJS)
    $(CC) $(LDFLAGS) $(LDLIBS) $(LDLIBS_EXEC) -o $@ $(OBJS)

%.o:%.c
    $(CC) $(LDFLAGS) $(LDLIBS) $(LDLIBS_OBJS) -c $< -o $@

clean:
    -rm -f $(EXEC) *.elf *.gdb *.o

#---------------------------------------------------------------------------------#

        保存并命名为Makefile(文件名不能更改),将当前目录设置为/home/armwork/exc目录,上传Makefile文件到当前目录中,输入make命令,编译测试程序main.c文件,生成main.o目标文件和main可执行文件。显示输出以下结果:

arm-elf-gcc -Os -g -Dlinux -D__linux__ -Dunix -D__uClinux__ -DEMBED -I/usr/src/uClinux-dist/lib/uClibc/include -I/usr/src/uClinux-dist/lib/libm -I/usr/src/uClinux-dist/lib/libcrypt_old -I/usr/src/uClinux-dist -fno-builtin -nostartfiles -D__PIC__ -fpic -msingle-pic-base -I/usr/src/uClinux-dist/linux-2.4.x/include -c -c main.c -o main.o
arm-elf-gcc -Os -g -Dlinux -D__linux__ -Dunix -D__uClinux__ -DEMBED -I/usr/src/uClinux-dist/lib/uClibc/include -I/usr/src/uClinux-dist/lib/libm -I/usr/src/uClinux-dist/lib/libcrypt_old -I/usr/src/uClinux-dist -fno-builtin -nostartfiles -D__PIC__ -fpic -msingle-pic-base -I/usr/src/uClinux-dist/linux-2.4.x/include -Wl,-elf2flt /usr/src/uClinux-dist/lib/uClibc/lib/crt0.o /usr/src/uClinux-dist/lib/uClibc/lib/crti.o /usr/src/uClinux-dist/lib/uClibc/lib/crtn.o -L/usr/src/uClinux-dist/lib/uClibc/. -L/usr/src/uClinux-dist/lib/uClibc/lib -L/usr/src/uClinux-dist/lib/libm -L/usr/src/uClinux-dist/lib/libnet -L/usr/src/uClinux-dist/lib/libdes -L/usr/src/uClinux-dist/lib/libaes -L/usr/src/uClinux-dist/lib/libpcap -L/usr/src/uClinux-dist/lib/libcrypt_old -L/usr/src/uClinux-dist/lib/libssl -L/usr/src/uClinux-dist/lib/zlib -lc -o main main.o

        编译后,该目录多出几个文件。用 ls 命令查看详细文件信息,显示结果如下:

-rwxr--r--    1 root     root        29464 5月 6 11:34 main
-rw-r--r--    1 root     root          515 5月 5 17:31 main.c
-rwxr-xr-x    1 root     root       742336 5月 6 11:34 main.gdb
-rw-r--r--    1 root     root         6828 5月 6 11:34 main.o
-rw-r--r--    1 root     root          951 5月 5 14:42 Makefile

从显示结果可以看出,现在的main可执行文件只有宿主机的root用户拥有可执行权限,其它用户只拥有只读权限,使用chmod命令更改权限:

# chmod 755 main

用 ls 命令查看修改后的文件详细信息为:

-rwxr-xr-x    1 root     root        29464 5月 6 11:34 main

这样,目标板也拥有对该文件的可执行权限了。

        现在回过头来再看目标板,通过超级终端,设置当前目录为/mnt/exc目录,执行main文件(输入./main命令),一切正常则可以听到测试程序所设计的:蜂鸣器每秒响一声,共响三声,同时超级终端有如下显示输入:

beep on!
beep off!
beep on!
beep off!
beep on!
beep off!
Success!

其中最后一条“Success! ”是在测试程序主函数里倒数第二条代码实现的(见前面的测试程序),其它显示是在驱动程序中beep_ioctl()函数里实现的。

        至此,测试工作完成。

        五、总结

        本文从编写驱动程序开始到编译驱动程序、加载模块一直到测试驱动程序,详细的介绍了如何在ARM的uClinux系统环境下加载模块化驱动程序及具体实现过程,完整阐述了模块化驱动程序的编写方法及注意事项,对ARM-Linux的初学者来说能起到一定的引导作用。而本文对Makefile文件的编写及分析没有进行介绍,在以后的文章中会详细介绍Makefile文件的编写。以上所完成的实验过程是在MagicARM2200教学试验开发平台实验测试通过的,所将内容大多是我的个人理解,而我也只是初学者,因此纰漏在所难免,也望各位读者指教。

        全文完

阅读(2226) | 评论(1) | 转发(1) |
给主人留下些什么吧!~~

chinaunix网友2010-10-15 16:56:21

很好的, 收藏了 推荐一个博客,提供很多免费软件编程电子书下载: http://free-ebooks.appspot.com