引言 ARM 器件是近年来兴起的一种低功耗、高功效的嵌入式处理器。S3C4510B 是一款针对网络处理而推出的专用处理器。在利用S3C4510B 处理器构成的嵌入式系统中,为了保证系统的可靠稳定,多数都需要看门狗;同时,在某些应用领域还需要时钟和日历服务。Dallas公司的DS1284 集成了上面两个功能。 在嵌入式系统应用越来越广泛的同时,嵌入式应用也变得越来越复杂。许多嵌入式系统都不得不借助于专用的操作系统来支撑自己的应用。uClinux 作为类Unix 操作系统,继承了Linux 的各种优秀品质,成为首选的嵌入式操作系统。本文以uClinux 为背景,以S3C4510B 为目标处理器,介绍uClinux 下DS1284 设备驱动的开发。 1 硬件设计 1.1 硬件描述 DS1284 是美国Dallas 公司推出的一款集成日历和看门狗功能的芯片。该芯片内部集成了实时时钟、静态RAM(内部寄存器)等, 支持电池供电可以保证主机掉电后继续工作。DS1284 包含了64 个8 位宽的寄存器,其中50 个可供用户存储一些需要掉电保护的数据,所有这些寄存器都可以通过外部总线直接访问。通过访问寄存器可以得到时钟、日历等信息,还可以设置定时报警和看门狗定时功能。所有这些寄存器的数据都以BCD 码方式保存和读取。如果用户需要,DS1284还可以输出1024H z 的方波信号。在DS1284 中,前14 个寄存器为功能寄存器。各个寄存器保存内容的意义如表1 所列。 命令寄存器的内部定义如表2 所列。 1.2 硬件连接 主机处理器采用的是三星公司出品的网络型ARM处理器S3C4510B。S3C4510B 处理器同DS1284 的连接电路如图1 所示。 2 软件设计 操作系统的作用之一就是向用户隐藏硬件设备的特殊性, 使应用程序的开发与低层物理设备无关。设备驱动程序就是连接应用程序和具体物理设备的桥梁。在uClinux 中设备可以分成三种:字符型设备、块设备、网络设备。DS1284 可以认为是一种字符型设备。uClinux 中所有的设备都被看成是一个文件。因此了解uClinux 下的设备驱动程序, 首先要了解内核中与设备相关的一些数据结构。
2.1 内核中设备数据结构 在应用程序里,访问设备文件的接口是标准的和统一的。一般而言,open、release、read、write、ioctl等都是对设备文件常用的操作。不同的设备有不同的实现方式。uClinux 中,通过一个结构体记录了每一种设备具体操作函数的函数指针, 以便在具体调用中转入到实际操 作的函数中。File_operations 结构体负责记录这些信息,其结构如下: structfile_operations { struct module *owner; loff_t(*llseek)(structfile*, loff_t, int); ssize_t(*read)(structfile*, char*, size_t, loff_t*); ssize_t(*write)(structfile*, constchar*, size_t, loff_t*); int(*readdir)(structfile*, void*, filldir_t); unsignedint(*poll)(structfile*, structpoll_table_struct*); int(*ioctl)(structinode*, structfile*, unsignedint, unsigned long); int(*mmap)(structfile*, structvm_area_struct*); int(*open)(structinode*, structfile*); int(*flush)(structfile*); int(*release)(structinode*, structfile*); int(*fsync)(structfile*, structdentry*, intdatasync); int(*fasync)(int, structfile*, int); int(*lock)(structfile*, int, structfile_lock*); ssize_t(*readv)(structfile*, conststructiovec*, unsignedlong, loff_t*); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t*); ssize_t(*sendpage)(structfile*, structpage*, int, size_t, loff_t *, int); unsignedlong(*get_unmapped_area)(structfile*, unsignedlong, unsignedlong, unsignedlong, unsignedlong); }; 在这个结构体中, 定义了各类操作函数的类型和入口参数, 常用的操作函数意义如下。 ① llseek,移动文件指针的位置。显然只能用于可以随机存取的设备。 ② read,进行读操作。参数buf 为存放读取结果的缓冲区,count 为所要读取的数据长度。返回值为负表示读取操作发生错误, 否则返回实际读取的字节数。 ③ write,进行写操作,与read 类似。 ④ readdir,取得下一个目录入口点,只有与文件系统相关的设备驱动程序才可使用。 ⑤ select,进行选择操作。如果驱动程序没有提供select 入口,select 操作将会认为设备已经准备好进行任何的I/O 操作。 ⑥ ioctl,进行读、写以外的其它操作,参数cmd 为自定义的命令。 ⑦ mmap , 用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。 ⑧ open,打开设备准备进行I/O 操作。返回0 表示打开成功, 返回负数表示失败。如果驱动程序没有提供open 入口,则只要/dev/driver 文件存在就认为打开成功。 ⑨ release,即close操作。 这里要说明的是, 并不是所有的接口函数都需要在驱动程序中实现。对于设备不支持的操作,可以将对应的设备接口函数指针置为空。在编写设备驱动程序中,只需要声明一个结构体,然后用实际处理函数的指针初始化该结构体就可以了。对DS1284 的结构体声明可声明如下(未说明部分自动置空)。 structfile_operationsds1284_fops={ open: ds1284_open, release: ds1284_release, read: ds1284_read, write: ds1284_write, llseek: ds1284_llseek, };
2.2 设备的注册 在uClinux 系统里,通过调用下面这个函数向系统注册字符型设备。 int register_chrdev(unsigned int major, const char *name,struct file_operations*fops) 其中,major 是为设备驱动程序向系统申请的主设备号。如果为0 , 则系统为此驱动程序动态地分配一个主设备号,name 是设备名,fops 就是前面所说的对各个调用的入口点的说明。此函数返回0 , 表示成功;返回-EINVAL,表示申请的主设备号非法。主设备号大于系统所允许的最大设备号或所申请的主设备号正在被其它设备驱动程序使用时, 将返回- EBUSY 。如果是动态分配主设备号成功, 此函数将返回所分配的主设备号。如果register_chrdev 操作成功,设备名就会出现在/proc/devices 文件里。初始化部分一般还负责给设备驱动程序申请系统资源,包括内存、中断、时钟、I/O 端口等,这些资源也可以在open 子程序或别的地方申请。在这些资源不用的时候, 应该释放它们,以利于资源的共享。 DS1284 的设备注册函数设计如下。 intds1284_init(void){ intresult; result=register_chrdev(WDT_MAJOR,"ds1284",&ds1284_fops); returnresult; } 2.3 I/O 端口的申请 任何进程都可以访问任何一个I / O 端口。此时系统无法保证对I / O 端口的操作不会发生冲突, 甚至会因此而使系统崩溃。因此,在使用I / O 端口前, 也应该检查此I / O 端口是否已有别的程序在使用, 若没有, 再把此端口标记为正在使用, 在使用完以后释放它。编写驱动程序时需要用到如下几个函数: int check_region(unsignedintfrom, unsignedintextent); void request_region(unsignedintfrom, unsignedintextent,constchar *name); void release_region(unsignedintfrom, unsignedintextent); 调用这些函数时的参数为:from 表示所申请I/O 端口的起始地址;extent 为所要申请的从from 开始的端口数;name 为设备名,将会出现在/proc/ioports 文件里。 check_region 返回0 表示I/O 端口空闲,否则为正在被使用。在申请了I/O 端口之后, 就可以用如下几个函数来访问I/O 端口: inline unsigned char inb(unsignedshortport); inline unsigned char inb_p(unsignedshortport); inline void outb(charvalue, unsignedshortport); inline void outb_p(charvalue, unsignedshortport); 其中inb_p 和outb_p 插入了一定的延时,以适应某些慢的I/O 端口。 在用户程序调用read 、write 时,因为进程的运行状态由用户态变为核心态, 地址空间也变为核心地址空间。而read、write 中的参数buf 是指向用户程序的私有地址空间, 所以不能直接访问, 必须通过以下两个系统函数来访问用户程序的私有地址空间。 void copy_from_user(void * to,const void * from,unsigned long n); void copy_to_user(void * to,const void * from,unsigned long n); copy_from_user 由用户程序地址空间往核心地址空间复制,copy_to_user 则反之。参数to 为复制的目的指针,from 为源指针,n 为要复制的字节数。
2.4 定时机制的实现 为防止看门狗电路意外复位, 系统需要在固定的时间间隔内重置看门狗计数器的值,因此在DS1284 系统中就需要用到定时器。在uClinux 系统中,时钟是由系统接管的。如果设备驱动程序中需要使用时钟的话,就需要向系统申请定时器资源。定时器部分的系统调用主要由以下三个函数完成: void add_timer(structtimer_list* timer); int del_timer(structtimer_list* timer); inline void init_timer(structtimer_list* timer); 其中timer_list 结构体的内容如下所示: structtimer_list { structtimer_list*next; structtimer_list*prev; unsignedlongexpires; unsignedlongdata; void(*function)(unsignedlongd); }; 其中expires 是要执行function 的时间。系统核心有一个全局变量JIFFIES,表示当前时间。一般在调用add_timer 时,jiffies=JIFFIES+num,表示在num个系统最小时间间隔后执行function 。系统最小时间间隔与所用的硬件平台有关,在核心里定义了常数HZ ,表示1s 内最小时间间隔的数目,则num * HZ 表示num 秒。系统计时到预定时间就调用function ,并把此子程序从定时队列里删除,因此如果想要每隔一定时间间隔执行一次的话,就必须在function 里再一次调用add_timer。function的参数d 即为timer 里面的data 项。DS1284 的定时函数实现如下: static struct timer_listds1284_timer; /*在ds1284_open函数中申请定时器资源*/ init_timer(&ds1284_timer); ds1284_timer.expires=jiffies+WDT_NUM*HZ/2; ds1284_timer.function=ds1284_timeout; add_timer(&ds1284_timer); void ds1284_timeout(unsigned long d){ unsignedcharvalue1,value2; del_timer(&ds1284_timer); ds1284_timer.expires=jiffies+WDT_NUM*HZ/2; value1=inb(WDT_ALARM_L); //读取看门狗计时器的值, //则自动更新计时器 value2=inb(WDT_ALARM_H); add_timer(&ds1284_timer); } 结语 嵌入式系统的应用以稳定为首要目标,uClinux 在扩展系统性能的同时保持了系统的稳定性。DS1284 的加入不但从硬件上保证了系统的可自恢复性, 而且扩展了系统的使用背景,可以作为一些无人看管设备且有时间记录要求的嵌入式系统平台。
|