uCLinux是一种面向嵌入式微处理器的微型操作系统,由于其源代码开放和功能齐备,已经在嵌入式操作系统中占有重要地位。介绍了在uCLinux版本2.4.26上如何实现可加载的设备驱动程序的设计步骤及其程序设计要点,以及在S3C4510b上实现LED输出和开关量输入的可加载内核模块驱动程序的实现过程和应用层测试程序。 0 引言 随着嵌入式微处理器技术和嵌入式操作系统的快速发展,嵌入式系统已广泛渗透到仪器仪表、工业控制等各个领域。uClinux以其源代码开放和功能齐备的特性在嵌入式操作系统中独树一帜,在嵌入式系统中得到了广泛应用,各种硬件也需要应用在嵌入式系统中以实现系统功能,这样就必须有相应的驱动程序支持这些硬件,以满足系统扩展功能的要求。在uClinux版本2.0以前实现驱动程序传统的做法是把驱动程序写成静态加载模块,每次修改源代码后调试都需要编译一次内核,这样导致驱动程序的调试效率十分低下。为了解决这个问题,本文采用了稳定的内核版本2.4.26的uClinux源代码进行驱动程序的开发,并采用可加载内核模块的方式进行设备驱动程序的调试,在内核运行时可以 动态的加载和卸载。本文主要介绍在uClinux操作系统下如何实现可加载的设备驱动程序的配置、编写和调试步骤。 uClinux是针对嵌入式控制领域的嵌入式Linux操作系统,系统结构上继承了Linux内核的绝大部分特性,在uClinux分发包中还包括很多成 熟的应用层程序,包括busybox、telnet、ftp等。由于现有的uClinux是为了没有内存管理单元MMU(memory manager unit)的嵌入式微处理器而设计的,所以有无MMU支持是Linux与uClinux的基本差异。由于没有内存管理单元,uClinux上的应用程序可直接访问物理存储器和外围设备端口,这使原有在嵌入式微处理器编写的程序可很容易移植到uClinux上使用。 1 可加载的设备驱动程序 设备驱动程序是操作系统内核和机器硬件间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个文件,应用程序可像操作普通文件一样对硬件设备进行操作。 在uClinux操作系统下有字符设备和块设备两类主要的设备文件类型。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了;块设备利用一块系统内存作为缓冲区,当用户进程对设备请求满足用户要求时,就返回请求的数据,它是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。为说明此原理,本文探讨把一个8盏LED和8个普通按钮作为字符设备在uClinux中使用。 Linux是一个高度模块化的操作系统,uClinux下驱动程序也是以模块形式存在的。在uClinux下对驱动程序加载有两种方式:静态编译到内核和动态加载到内核。在uClinux V2.4之前的版本只支持静态编译到内核,V2.4以后,uClinux支持了动态加载到内核的功能,使驱动程序的调试变得与调试应用程序一样方便快捷。可加载内核驱动程序的开发步骤一般分为3步: ①采用uClinux V2.4.26以后的稳定版本进行内核编译,在编译时让内核支持动态加载; ②可加载内核模块驱动程序的编写; ③把驱动程序通过FTP传到目标板中用命令行工具安装,通过应用层测试程序进行测试,改进。 下面说明可加载的内核驱动程序的开发步骤。 1.1 uClinux内核的配置 首先是配置并编译内核,使内核支持串行接口,FTP,支持内核的命令,如:insmod,lsmod,rmmod。接下来运行make dep和make all用于生成目标板的内核映像文件image.ram和image.rom,这里把image.rom写入Flash。写入完成后,通过连接到uClinux目标板的串口终端运行并配置uClinux,正确设置网络IP地址,使uClinux的目标板和开发用主机能够互相用FTP访问,这里把主机IP设为210.46.109.26,uClinux的目标板IP地址设为210.46.109.25,通过ping命令确认网络通畅,在串口终端上运行“FTP 210.46.109.26”能够正常登陆主机并下载文件。 不代参数执行insmod,lsmod,rmmod命令能够得到正常的错误提示。至此第一步的准备工作完成。
1.2 可加载内核模块驱动程序的编写 在uClinux中,为了让应用程序对底层硬件有一个统一的访问标准,必须对每个在uClinux中应用的硬件编写驱动程序,驱动程序加载成功后,应用程序就可以像读写普通文件一样来通过驱动程序访问硬件设备了,所以驱动程序必须处理一些标准的文件操作,常见的包括“open”,“read”,“write”等。 编写驱动程序主要是编写当“Open”,“Read”,“Write”等操作产生时的相应函数,这些函数的入口地址在程序中被定位在uClinux的一个“file-operations”结构体中,例如: struct file-operations lcd-fileops={ open:lcd-open, release:lcd-release, read:led-read, write:led-write, }; lcd-fileops为此结构体的名称,均为led-open,lcd-release等均为相应的函数名称,当此驱动程序加载后,若应用程序要打开此设备,操作系统会向驱动程序发出“open”请求,驱动程序会按lcd-fileops中的定义调用open对应的处理函数lcd-open实现应用程序要的打开功能,例如: int lcd-open(struct inode *inode,struct file *file) { MOD-INC-USE-COUNT; printk("charlcd device opened./n"); return 0; } struct inode和struct file是操作系统传进来的指针,一般情况不需修改,MOD-INC-USE-COUNT是使此设备计数器+1,在调用release操作时会-1(对应操作为MOD-DEC-USE-COUNT),uClinux通过这种简单的机制以防止文件在关闭前驱动程序模块被卸载出内核。每次成功的设备打开操作后都应该执行MOD-INC-USE-COUNT。为简单起见,没有进行设备有效性检查,直接调用了,printk是调试驱动程序时一个内核输出函数(在内核中不能用printf函数),它执行时会向默认的输出设备输出相应信息,如,本例执行时,会向串口终端输出字符串“charlcd device opened.”并换行。 文件操作函数led-read和led-write,分别对应“read”和“write”操作,其中led-read的函数例子如下: ssize-t lcd-read(struct file* file,char *userBur,size-t sz-buf,loff-t *lf) { IOMODE= INPUTMODE; userBuf[0]=IOPDATA; printk("Reading data from kernel process/n"); return 0; } userBuf是驱动程序与应用程序的数据接口,其长度为sz-buf。在执行读操作时应该把硬件“读”到的数据放在userBuf中。应用程序在对此硬件执行“read”操作后会得到userBuf中的数据。本例是读取外部8个开关的状态,把输入输出端口模式寄存器IOPMODE定义为输入(INPUTMODE),并把输入输出端口数据寄存器的值(当前开关状态)赋给userBuf[0]。Lcd-write的函数例子如下: ssize-t lcd-write(struct file*file,const char *buf,size-t sz-buf,loff-t *lf) { int count; if(bur==NULL)return -1; IOPMODE=OUTPUTMODE; IOPDATA=buf[0]; printk("Writing user data to kernel process/n"); return 0; } buf是应用程序与驱动程序的接口,其长度为sz-buf。在执行操作时,由上层应用程序来决定buf的内容,由此函数内的代码来决定如何把buf的内容“写”到硬件设备中。本例中,把Samsung4510b的输入输出端口模式寄存器(IOPMODE)置为输出状态来控制8个发光二极管,通过把buf[0]的数据写入输入输出端口寄存器(IOPDATA)来决定外部8个发光二极管的亮灭。 以上操作对于动态和静态两种加载方式的驱动程序都是通用的,对可加载内核模块驱动程序,还必须在程序中定位以下两个操作的位置:module-init和module-exit。在程序中如下表示: module-init(lcd-init); module-exit(lcd-clearup); 说明module-init操作的实现函数是lcd-init,module-exit操作的实现函数是(lcd-clearup),由于lcd-init是模块注册和初始化函数,使用完后就可以释放占有的资源,在执行时必须放到内核内存区的.text.init区,所以,必须在lcd-init函数定义前加入static int --init声明,一个实际的例子如下: int lcd-devNo; static int --init lcd-init(void) { int iResult==0; printk("Registering charlcd device into /dev /n"); iResult = register-chrdev(254,"charlcd",&lcd-fileops); if(iResult==254) { printk("Charlcd device is registered./n"); led-devNo = iResult; return iResult; } else { printk("Error occurred while Registering charlcd device./n"); return iResult; } } 若register-chrdev执行成功,会返回注册成功的主设备号(major number),把此主设备号保存到lcd-devNo是将来为动态注销此驱动程序时使用的,因为动态注销此驱动程序需要主设备号,动态注销函数是module-exit()中指定的函数,本例中是“lcd-clearup”,例如: int led-clearup(void) { int iResult=0; if((iResult= unregister-chrdev(1ed-devNo,"charlcd"))==0) printk("Remove the charlcd device from kernel./n"); else printk("Error occurred while Removing the charlcd device./n"); return iResult; } lcd-devNo是当register-chrdev执行成功时赋的值,若unregister-chrdev函数执行成功,返回0,否则返回出错代码,交由操作系统处理。 综上所述,一个典型的可动态加载的内核驱动程序模块的代码包括两大部分: ①file-operations结构体的填充和函数实现; ②module-init()和module-exit()函数参数指针的填充和函数实现。 对于静态内核驱动程序而言不需要第二部分,但是需要手动修改uClinux的配置源代码,加入模块注册信息,比较烦琐,容易出错。而可动态加载的内核驱动程序模块则不会出现此问题。 把编辑完的代码通过arm-elf-gcc编译成目标文件(.o文件)就完成了可加载内核驱动程序的生成,常用的编译指令文件Makefile的内容如下: CC=arm-elf-gcc//指明使用的交叉编译器 OBJS=led.o//指明目标文件的名称 SOCS=led.c//指明源程序文件的名称 LD= arm-elf-gcc//指明使用的连接器 LDFLAGS = --DMODULE -D --KERNEL -- D --linux --C all: $(CC) -o $(OBJS) $(LDFLAGS) $(SOCS) cp $(OBJS)/root/led.o clean: -rm -f $(EXEC) *.elf *.gdb *.o
1.3 驱动程序的安装与测试 把驱动程序通过FTP传到目标板中安装,通过应用程序对例子进行测试,改进。 首先通过串口终端输入指令把上一步生成的目标文件(这里假设为led.o)以二进制形式(命令为bina)用FTP下载到uClinux的/var目录下(因为此 目录是可写的),而后用insmod命令加载led.o驱动程序,此时操作系统会调用驱动程序的module-init操作,对本例是执行lcd.init函数完成驱动程序的注册。insmod命令执行正常,会在串口终端输出“Charlcd device is registered.”字符串,表明此字符设备已经正确加载,为了确认此设备是否真的已经在操作系统中存在,可用lsmod命令或查看/proc/devices文件的内容来确认。/proc/devices文件是uClinux保存当前系统设备的一个特殊文件,以纯文本形式存储。例如,执行“cat/proc/devices”,如正常,系统会有以下输出: Character devices: 1 mem 2 pty 254 charlcd -这是驱动程序中申请的设备主编号和设备名称。 此步说明驱动程序已经成功地动态加载到内核,如何用应用程序访问驱动程序呢?就是采用操作系统的文件操作就可以访问驱动程序,进而访问硬件设备,下面给出一个简单的应用程序例程片断,用来访问之前动态加载的驱动程序: int main(void) { int i; char buf[20]; int iResult=open("/dev/charlcd",O-RDWR); if(iResult==0) { printf("Open/dev/charlcd ok!/n"); read(iResult,buf,1);//读取8个开关状态 printf("Key Status:%.2x /n",buf[O]); buf[0]=0x55; printf("LED Status:%.2x /n",buf[O]); write(iResult,buf,1);//设置8个发光二极管亮灭 close(iResult); } else { printf("Error on open/dev/charlcd!/n"); } return 1; } read(iResult,bur,i)函数执行时会调用驱动程序的read操作对应的函数lcd-read(struct file *file,char *userBuf,size-t sz-buf,loff-t *lf)进行操作,把iResult传给file、buf传给userBuf、i传给sz-buf;write(iResult,buf,1)函数执行时会调用驱动程序的write操作对应的函数led write(struct file*file,const char* buf,size-t sz.buf,loff-t *lf)进行操作,把iResult传给file、buf传递给buf、i传递给sz-buf。 通过以上方法可像调试应用程序一样来迅速测试驱动程序,通过调试输出语句printk可调试驱动程序,当然驱动程序调试完毕后,发布时还应该用静态编译到内核的方式把驱动程序固化到内核中,uClinux下把驱动程序静态编译到内核已有文献介绍 ,本文不再赘述。 2 结语 介绍了uClinux下可加载内核驱动程序模块的实现过程,这种方式与原有静态编译到内核的方式比较,在驱动程序测试,调试阶段节省了大量的时间,极大地提高了驱动程序的调试效率,使具体硬件设备尽早在uClinux的产品中使用成为可能。
|