46
6 第第第 Linux 第第第第第第

第 6 章 嵌入式 Linux 驱动程序开发

  • Upload
    pilar

  • View
    100

  • Download
    6

Embed Size (px)

DESCRIPTION

第 6 章 嵌入式 Linux 驱动程序开发. 6.1 嵌入式 Linux 的设备管理. Linux 将设备分成两大类:一类是块设备,类似磁盘以记录块或扇区为单位,成块进行输入 / 输出的设备;另一类是字符设备,类似键盘以字符为单位,逐个进行输入 / 输出的设备。网路设备是介于块设备和字符设备之间的一种特殊设备。 块设备接口仅支持面向块的 I/O 操作,所有 I/O 操作都通过在内核地址空间中的 I/O 缓冲区进行,它可以支持随机存取的功能。文件系统通常都建立在块设备上。 - PowerPoint PPT Presentation

Citation preview

Page 1: 第 6 章   嵌入式 Linux 驱动程序开发

第 6章 嵌入式 Linux驱动程序开发

Page 2: 第 6 章   嵌入式 Linux 驱动程序开发

6.1 嵌入式 Linux 的设备管理Linux 将设备分成两大类:一类是块设备,类似磁盘以记录块或扇区为单位,成块

进行输入 / 输出的设备;另一类是字符设备,类似键盘以字符为单位,逐个进行输入 /输出的设备。网路设备是介于块设备和字符设备之间的一种特殊设备。

块设备接口仅支持面向块的 I/O 操作,所有 I/O 操作都通过在内核地址空间中的 I/O 缓冲区进行,它可以支持随机存取的功能。文件系统通常都建立在块设备上。

字符设备接口支持面向字符的 I/O 操作,由于它们不经过系统的快速缓存,所以它们负责管理自己的缓冲区结构。字符设备接口只支持顺序存取的功能,一般不能进行任意长度的 I/O 请求,而是限制 I/O 请求的长度必须是设备要求的基本块长的倍数。

处理器与设备间数据交换方式处理器与外设之间传输数据的控制方式通常有 3 种:查询方式、中断方式和直接

内存存取( DMA )方式。1 .查询方式设备驱动程序通过设备的 I/O 端口空间,以及存储器空间完成数据的交换。例如,

网卡一般将自己的内部寄存器映射为设备的 I/O 端口,而显示卡则利用大量的存储器空间作为视频信息的存储空间。利用这些地址空间,驱动程序可以向外设发送指定的操作指令。通常来讲,由于外设的操作耗时较长,因此,当处理器实际执行了操作指令之后,驱动程序可采用查询方式等待外设完成操作。

驱动程序在提交命令之后,开始查询设备的状态寄存器,当状态寄存器表明操作完成时,驱动程序可继续后续处理。查询方式的优点是硬件开销小,使用起来比较简单。但在此方式下, CPU 要不断地查询外设的状态,当外设未准备好时,就只能循环等待,不能执行其他程序,这样就浪费了 CPU 的大量时间,降低了处理器的利用率。

Page 3: 第 6 章   嵌入式 Linux 驱动程序开发

2 .中断方式查询方式白白浪费了大量的处理器时间,而中断方式才是多任务操作系统中最有效利

用处理器的方式。当 CPU 进行主程序操作时,外设的数据已存入端口的数据输入寄存器,或端口的数据输出寄存器已空,此时由外设通过接口电路向 CPU 发出中断请求信号。 CPU 在满足一定条件下,暂停执行当前正在执行的主程序,转入执行相应能够进行输入 / 输出操作的子程序,待输入 / 输出操作执行完毕之后, CPU 再返回并继续执行原来被中断的主程序。这样, CPU 就避免了把大量时间耗费在等待、查询外设状态的操作上,使其工作效率得以大大提高。中断方式的原理示意图如图 6.1 所示。

能够向 CPU 发出中断请求的设备或事件称为中断源。中断源向 CPU 发出中断请求,若优先级别最高,则 CPU 在满足一定的条件时,可中断当前程序的运行,保护好被中断的主程序的断点及现场信息,然后根据中断源提供的信息,找到中断服务子程序的入口地址,转去执行新的程序段,这就是中断响应。 CPU响应中断是有条件的,如内部允许中断、中断未被屏蔽、当前指令执行完等。 CPU响应中断以后,就会中止当前的程序,转去执行一个中断服务子程序,以完成为相应设备的服务。

系统引入中断机制后, CPU 与外设处于“并行”工作状态,便于实现信息的实时处理和系统的故障处理。

Page 4: 第 6 章   嵌入式 Linux 驱动程序开发

3 .直接访问内存( DMA )方式利用中断,系统和设备之间可以通过设备驱动程序传送数据,但是,当传送的数据量很大时,因为中断处理上的延迟,利用中断方式的效率会大大降低。而直接内存访问( DMA )可以解决这一问题。 DMA 可允许设备和系统内存间在没有处理器参与的情况下传输大量数据。设备驱动程序在利用 DMA 之前,需要选择 DMA 通道并定义相关寄存器,以及数据的传输方向,即读取或写入,然后将设备设定为利用该 DMA 通道传输数据。设备完成设置之后,可以立即利用该 DMA 通道在设备和系统的内存之间传输数据,传输完毕后产生中断以便通知驱动程序进行后续处理。在利用 DMA 进行数据传输的同时,处理器仍然可以继续执行指令。

Page 5: 第 6 章   嵌入式 Linux 驱动程序开发

设备驱动程序的概念 设备驱动程序实际是处理和操作硬件控制器的软件,从本质上讲,是内核中具有最高

特权级的、驻留内存的、可共享的底层硬件处理例程。驱动程序是内核的一部分,是操作系统内核与硬件设备的直接接口,驱动程序屏蔽了硬件的细节,完成以下功能:

对设备初始化和释放; 对设备进行管理,包括实时参数设置,以及提供对设备的操作接口; 读取应用程序传送给设备文件的数据或者回送应用程序请求的数据; 检测和处理设备出现的错误。Linux 操作系统将所有的设备全部看成文件,并通过文件的操作界面进行操作。对用户

程序而言,设备驱动程序隐藏了设备的具体细节,对各种不同设备提供了一致的接口,一般来说,是把设备映射为一个特殊的设备文件,用户程序可以像对其他文件一样对此设备文件进行操作。这意味着:

由于每一个设备至少由文件系统的一个文件代表,因而都有一个“文件名”。 应用程序通常可以通过系统调用 open()打开设备文件,建立起与目标设备的连接。 打开了代表着目标设备的文件,即建立起与设备的连接后,可以通过 read() 、 writ

e() 、 ioctl() 等常规的文件操作对目标设备进行操作。设备文件的属性由三部分信息组成:第一部分是文件的类型,第二部分是一个主设备

号,第三部分是一个次设备号。其中类型和主设备号结合在一起惟一地确定了设备文件驱动程序及其界面,而次设备号则说明目标设备是同类设备中的第几个。

由于 Linux 中将设备当做文件处理,所以对设备进行操作的调用格式与对文件的操作类似,主要包括 open() 、 read() 、 write() 、 ioctl() 、 close() 等。应用程序发出系统调用命令后,会从用户态转到核心态,通过内核将 open() 这样的系统调用转换成对物理设备的操作。

Page 6: 第 6 章   嵌入式 Linux 驱动程序开发

驱动程序结构 1 .自动配置和初始化子程序,用来检测所需驱动的硬件设备是否工作正常、对正常工作的设备及其相关驱动程序所需要的软件状态进行初始化。2 .服务于 I/O 请求的子程序,该子程序称为驱动程序的上半部。这部分程序在执行时,系统仍认为与进行调用的进程属于同一个进程,只是由用户态变成了核心态,可以在其中调用 sleep() 等与进程运行环境有关的函数。3 .中断服务程序,又称为驱动程序的下半部,由 Linux 系统来接收硬件中断,再由系统调用中断服务子程序。在系统内部, I/O 设备的存取通过一组固定的入口点来进行,入口点也可以理解为设备的句柄,就是对设备进行操作的基本函数。字符型设备驱动程序提供如下几个入口点: open 入口点。打开设备准备 I/O 操作。对字符设备文件进行打开操作,都会调用设备的 open 入口点。 open 子程序必须对将要进行的 I/O 操作做好必要的准备工作,如清除缓冲区等。如果设备是独占的,即同一时刻只能有一个程序访问此设备,则 open 子程序必须设置一些标志以表示设备处于忙状态。 close 入口点。关闭一个设备。当最后一次使用设备完成后,调用 close 子程序。独占设备必须标记设备方可再次使用。 read 入口点。从设备上读数据。对于有缓冲区的 I/O 操作,一般是从缓冲区里读数据。对字符设备文件进行读操作将调用 read 子程序。 write 入口点。往设备上写数据。对于有缓冲区的 I/O 操作,一般是把数据写入缓冲区里。对字符设备文件进行写操作将调用 write 子程序。 ioctl 入口点。执行读、写之外的操作。 select 入口点。检查设备,看数据是否可读或设备是否可用于写数据。 select 系统调用在检查与设备文件相关的文件描述符时使用 select 入口点。

Page 7: 第 6 章   嵌入式 Linux 驱动程序开发

struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char *, size_t, loff_t *);ssize_t (*write) (struct file *, const char *, size_t, loff_t *);int (*readdir) (struct file *, void *, filldir_t);unsigned int (*poll) (struct file *, struct poll_table_struct *);int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, struct dentry *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *,unsigned long, unsigned long, unsigned long, unsigned long);};

lseek ,移动文件指针的位置,只能用于可以随机存取的设备。 read ,进行读操作, buf 为存放读取结果的缓冲区, count 为所要读取的数据长度。 write ,进行写操作,与 read 类似。 select ,进行选择操作。 ioctl ,进行读、写以外的其他操作。 mmap ,用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。 open ,打开设备进行 I/O 操作。返回 0 表示成功,返回负数表示失败。 release ,即 close 操作。

Page 8: 第 6 章   嵌入式 Linux 驱动程序开发

struct inode称做索引节点数据结构,定义如下:struct inode {struct list_head i_hash;struct list_head i_list;struct list_head i_dentry;struct list_head i_dirty_buffers;struct list_head i_dirty_data_buffers;unsigned long i_ino;atomic_t i_count;kdev_t i_dev;umode_t i_mode;nlink_t i_nlink;uid_t i_uid;gid_t i_gid;kdev_t i_rdev;loff_t i_size;time_t i_atime;time_t i_mtime;time_t i_ctime;unsigned int i_blkbits;unsigned long i_blksize;unsigned long i_blocks;unsigned long i_version;struct semaphore i_sem;struct semaphore i_zombie;struct inode_operations *i_op;struct file_operations *i_fop;struct super_block *i_sb;wait_queue_head_t i_wait;struct file_lock *i_flock;struct address_space *i_mapping;struct address_space i_data;struct dquot *i_dquot[MAXQUOTAS];struct list_head i_devices;struct pipe_inode_info *i_pipe;struct block_device *i_bdev;struct char_device *i_cdev;unsigned long i_dnotify_mask; struct dnotify_struct *i_dnotify; unsigned long i_state;unsigned int i_flags;unsigned char i_sock;atomic_t i_writecount;unsigned int i_attr_flags;

__u32 i_generation;union {

struct minix_inode_info minix_i; struct ext2_inode_info ext2_i; struct ext3_inode_info ext3_i; struct hpfs_inode_info hpfs_i; struct ntfs_inode_info ntfs_i; struct msdos_inode_info msdos_i; struct umsdos_inode_info umsdos_i; struct iso_inode_infoisofs_i; struct nfs_inode_infonfs_i; struct sysv_inode_info sysv_i; struct affs_inode_info affs_i; struct ufs_inode_infoufs_i; struct efs_inode_infoefs_i; struct romfs_inode_info romfs_i; struct shmem_inode_info shmem_i; struct coda_inode_info coda_i; struct smb_inode_infosmbfs_i; struct hfs_inode_infohfs_i; struct adfs_inode_info adfs_i; struct qnx4_inode_info qnx4_i; struct reiserfs_inode_info reiserfs_i; struct bfs_inode_infobfs_i; struct udf_inode_infoudf_i; struct ncp_inode_infoncpfs_i; struct proc_inode_info proc_i; struct socket socket_i; struct usbdev_inode_info usbdev_i; struct jffs2_inode_info jffs2_i; void*generic_ip;

} u;};

Page 9: 第 6 章   嵌入式 Linux 驱动程序开发

struct file 主要用于与文件系统相关的设备驱动程序,可提供关于被打开的文件的信息,定义如下:

struct file {struct list_head f_list;struct dentry *f_dentry;struct vfsmount *f_vfsmnt;struct file_operations *f_op;atomic_t f_count;unsigned int f_flags;mode_t f_mode;loff_t f_pos;unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;struct fown_struct f_owner;unsigned int f_uid, f_gid;int f_error;unsigned long f_version;/* needed for tty driver, and maybe others */void *private_data;/* preallocated helper kiobuf to speedup O_DIRECT */struct kiobuf *f_iobuf;long f_iobuf_lock;};

在用户自己的驱动程序中,首先要根据驱动程序的功能,完成 file_operations 结构中函数的实现。不需要的函数接口可以直接在 file_operations 结构中初始化为 NULL 。file_operations 中的变量会在驱动程序初始化时,注册到系统内部。每个进程对设备的操作,都会根据主次设备号,转换成对 file_operations 结构的访问。

Page 10: 第 6 章   嵌入式 Linux 驱动程序开发

6.2 设备驱动程序的开发过程由于嵌入式设备由于硬件种类非常丰富,在默认的内核发布版中不一定包括所

有驱动程序。所以进行嵌入式 Linux 系统的开发,很大的工作量是为各种设备编写驱动程序。除非系统不使用操作系统,程序直接操纵硬件。嵌入式 Linux 系统驱动程序开发与普通 Linux 开发没有区别。可以在硬件生产厂家或者 Internet 上寻找驱动程序,也可以根据相近的硬件驱动程序来改写,这样可以加快开发速度。实现一个嵌入式 Linux 设备驱动的大致流程如下。

( 1 )查看原理图,理解设备的工作原理。一般嵌入式处理器的生产商提供参考电路,也可以根据需要自行设计。

( 2 )定义设备号。设备由一个主设备号和一个次设备号来标识。主设备号惟一标识了设备类型,即设备驱动程序类型,它是块设备表或字符设备表中设备表项的索引。次设备号仅由设备驱动程序解释,区分被一个设备驱动控制下的某个独立的设备。

( 3 )实现初始化函数。在驱动程序中实现驱动的注册和卸载。( 4 )设计所要实现的文件操作,定义 file_operations 结构。( 5 )实现所需的文件操作调用,如 read 、 write 等。( 6 )实现中断服务,并用 request_irq 向内核注册,中断并不是每个设备驱

动所必需的。( 7 )编译该驱动程序到内核中,或者用 insmod 命令加载模块。( 8 )测试该设备,编写应用程序,对驱动程序进行测试。

Page 11: 第 6 章   嵌入式 Linux 驱动程序开发

模块化驱动程序设计 在探讨模块之前,有必要先看一看内核模块与应用程序之间的区别。一个应用

从头到尾完成一个任务,而模块则是为以后处理某些请求而注册自己,完成这个任务后,它的“主”函数就立即中止了。然而,内核源码仅能连接编译到内核模块中,不像应用那样有众多的支持库,

内核能调用的仅是由内核开放出来的那些函数。由于没有库连接到模块中,所以源码文件不应该模块化任何常规头文件。与内核有关的所有内容都定义在目录 /usr/include/linux 和 /usr/include/asm 下的头文件中。

1 .内核空间和用户空间当谈到软件时,我们通常称执行态为“内核空间”和“用户空间”,在 Linux

系统中,内核在最高级执行,也称为“管理员态”,在这一级任何操作都可以执行。而应用程序则执行在最低级,所谓的“用户态”,在这一级处理器禁止对硬件的直接访问和对内存的未授权访问。模块是在所谓的“内核空间”中运行的,而应用程序则是在“用户空间”中运行的。它们分别引用不同的内存映射,也就是程序代码使用不同的“地址空间”。

Linux 通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。执行系统调用的内核代码在进程的上下文中执行,它执行调用进程的操作而且可以访问进程地址空间的数据。但处理中断与此不同,处理中断的代码相对进程而言是异步的,而且与任何一个进程都无关。模块的作用就是扩展内核的功能,是运行在内核空间的模块化的代码。模块的某些函数作为系统调用执行,而某些函数则负责处理中断。

Page 12: 第 6 章   嵌入式 Linux 驱动程序开发

各个模块被分别编译并链接成一组目标文件,这些文件能被载入正在运行的内核,或从正在运行的内核中卸载。必要时内核能请求内核守护进程 Kerneld对模块进行加载或卸载。根据需要动态载入模块可以保证内核达到最小,并且具有很大的灵活性。内核模块一部分保存在 Kernel 中,另一部分在 Modules包中。在项目一开始,很多地方对设备安装、使用和改动都是通过编译进内核来实现的,对驱动程序稍微做点改动,就要重新烧写一遍内核,而且烧写内核经常容易出错,还占用资源。模块采用的则是另一种途径,内核提供一个插槽,它就像一个插件,在需要时,插入内核中使用,不需要时从内核中拔出。这一切都由一个称为 Kerneld 的守护进程自动处理。

2 .模块化的优缺点内核模块的动态加载具有以下优点:将内核映像的尺寸保持在最小,并具有

最大的灵活性。这便于检验新的内核代码,而不需要重新编译内核并重新引导。但是,内核模块的引入也对系统性能和内存的利用有负面影响。装入的内核

模块与其他内核部分一样,具有相同的访问权限,由此可见,差的内核模块会导致系统崩溃。为了使内核模块能访问所有内核资源,内核必须维护符号表,并在加载和卸载模块时修改这些符号表。由于有些模块要求利用其他模块的功能,故内核要维护模块之间的依赖性。内核必须能够在卸载模块时通知模块,并且要释放分配给模块的内存和中断等资源。内核版本和模块版本的不兼容也可能导致系统崩溃,因此,严格的版本检查是必需的。尽管内核模块的引入同时带来不少问题,但是模块机制确实是扩充内核功能的一种行之有效的方法,也是在内核级进行编程的有效途径。

Page 13: 第 6 章   嵌入式 Linux 驱动程序开发

设备注册和初始化 设备的驱动程序在加载的时候首先需要调用入口函数 init_module() ,该函数最重要的

一个工作就是向内核注册该设备,对于字符设备调用 register_chrdev() 完成注册。 register_chrdev 的定义为: int register_chrdev(unsigned int major, const char *name, struct file_ operations *fops);

其中, major 是为设备驱动程序向系统申请的主设备号,如果为 0 ,则系统为此驱动程序动态分配一个主设备号。 name 是设备名, fops 是对各个调用的入口点说明。此函数返回 0 时表示成功;返回 -EINVAL ,表示申请的主设备号非法,主要原因是主设备号大于系统所允许的最大设备号;返回 -EBUSY ,表示所申请的主设备号正在被其他设备程序使用。如果动态分配主设备号成功,此函数将返回所分配的主设备号。如果 register_chrdev() 操作成功,设备名就会出现在 /proc/dvices 文件中。

Linux 在 /dev目录中为每个设备建立一个文件,用 ls –l 命令列出函数返回值,若小于0 ,则表示注册失败;返回 0 或者大于 0 的值表示注册成功。注册以后, Linux 将设备名与主、次设备号联系起来。当有对此设备名的访问时, Linux 通过请求访问的设备名得到主、次设备号,然后把此访问分发到对应的设备驱动,设备驱动再根据次设备号调用不同的函数。

当设备驱动模块从 Linux 内核中卸载,对应的主设备号必须被释放。字符设备在 cleanup_ module()函数中调用 unregister_chrdev() 来完成设备的注销。 unregister_chrdev()的定义为: int unregister_chrdev(unsigned int major, const char *name);

此函数的参数为主设备号 major 和设备名 name 。 Linux 内核把 name 和 major 在内核注册的名称对比,如果不相等,卸载失败,并返回 EINVAL ;如果major 大于最大的设备号,也返回 EINVAL 。

Page 14: 第 6 章   嵌入式 Linux 驱动程序开发

包括设备注册在内,设备驱动的初始化函数主要完成的功能是有以下 5项。( 1 )对驱动程序管理的硬件进行必要的初始化。对硬件寄存器进行设置。比如,设置中断掩码,设置串口的工作方式、并口的数据

方向等。( 2 )初始化设备驱动相关的参数。一般说来,每个设备都要定义一个设备变量,用以保存设备相关的参数。在这一步

骤里对设备变量中的项进行初始化。( 3 )在内核注册设备。调用 register_chrdev()函数来注册设备。( 4 )注册中断。如果设备需要 IRQ 支持,则要使用 request_irq()函数注册中断。( 5 )其他初始化工作。初始化部分一般还负责给设备驱动程序申请包括内存、时钟、 I/O 端口等在内的系

统资源,这些资源也可以在 open 子程序或者其他地方申请。这些资源不用时,应该释放,以利于资源的共享。若驱动程序是内核的一部分,初始化函数则要按如下方式声明:int __init chr_driver_init(void);其中 __init 是必不可少的,在系统启动时会由内核调用 chr_driver_init ,完成驱动

程序的初始化。当驱动程序是以模块的形式编写时,则要按照如下方式声明:int init_module(void)当运行后面介绍的 insmod 命令插入模块时,会调用 init_module函数完成初始化

工作。

Page 15: 第 6 章   嵌入式 Linux 驱动程序开发

中断管理 设备驱动程序通过调用 request_irq函数来申请中断,通过 free_irq 来释放中断。它们

在 linux/sched.h 中的定义如下:int request_irq(unsigned int irq,void (*handler)(int irq,void dev_id,struct pt_regs *regs),unsigned long flags,const char *device,void *dev_id);void free_irq(unsigned int irq, void *dev_id);

通常从 request_irq函数返回的值为 0 时,表示申请成功;负值表示出现错误。 irq 表示所要申请的硬件中断号。 handler 为向系统登记的中断处理子程序,中断产生时由系统来调用,调用时所带

参数 irq 为中断号, dev_id 为申请时告诉系统的设备标识, regs 为中断发生时寄存器内容。

device 为设备名,将会出现在 /proc/interrupts 文件里。 flag 是申请时的选项,它决定中断处理程序的一些特性,其中最重要的是决定中断

处理程序是快速处理程序( flag里设置了 SA_INTERRUPT )还是慢速处理程序(不设置SA_INTERRUPT )。

下面的代码将在 SBC-2410X 的 Linux 中注册外部中断 2 。eint_irq = IRQ_EINT2;set_external_irq (eint_irq, EXT_FALLING_EDGE,GPIO_PULLUP_DIS);ret_val = request_irq(eint_irq,eint2_handler, “S3C2410X eint2”,0);if(ret_val < 0){return ret_val;}

用来打开和关闭中断的函数如下:#define cli() _asm_ _volatile_ ("cli"::)#define sli() _asm_ _volatile_ ("sli"::)

Page 16: 第 6 章   嵌入式 Linux 驱动程序开发

设备驱动开发的基本函数 1 . I/O 口函数无论驱动程序多么复杂,归根结底,无非还是向某个端口或者某个寄存器位赋值,

这个值只能是 0 或 1 。接收值的就是 I/O 口。与中断和内存不同,使用一个没有申请的 I/O 端口不会使处理器产生异常,也就不会导致诸如“ segmentation fault” 一类的错误发生。由于任何进程都可以访问任何一个 I/O 端口,此时系统无法保证对 I/O 端口的操作不会发生冲突,甚至因此而使系统崩溃。因此,在使用 I/O 端口前,也应该检查此 I/O 端口是否已有别的程序在使用,若没有,再把此端口标记为正在使用,在使用完以后释放它。

这样需要用到如下几个函数:int check_region(unsigned int from, unsigned int extent);void request_region(unsigned int from, unsigned int extent,const char *name);void release_region(unsigned int from, unsigned int extent);

调用这些函数时的参数为: from 表示所申请的 I/O 端口的起始地址; extent 为所要申请的从 from 开始的端口数; name 为设备名,将会出现在 /proc/ioports 文件里; check_region 返回 0 表示 I/O 端口空闲,否则为正在被使用。在申请了 I/O 端口之后,可以借助 asm/io.h 中的如下几个函数来访问 I/O 端口:inline unsigned int inb(unsigned short port);inline unsigned int inb_p(unsigned short port);inline void outb(char value, unsigned short port);inline void outb_p(char value, unsigned short port);

其中 inb_p 和 outb_p插入了一定的延时以适应某些低速的 I/O 端口。

Page 17: 第 6 章   嵌入式 Linux 驱动程序开发

2 .时钟函数在设备驱动程序中,一般都需要用到计时机制。在 Linux 系统中,时钟是由系统接管

的,设备驱动程序可以向系统申请时钟。与时钟有关的系统调用有:#include <asm/param.h>#include <linux/timer.h>void add_timer(struct timer_list * timer);int del_timer(struct timer_list * timer);inline void init_timer(struct timer_list * timer);struct timer_list 的定义为:struct timer_list {struct timer_list *next;struct timer_list *prev;unsigned long expires;unsigned long data;void (*function)(unsigned long d);};其中, expires 是要执行 function 的时间。系统核心有一个全局变量 jiffies 表示当前时

间,一般在调用 add_timer 时 jiffies=JIFFIES+num ,表示在 num 个系统最小时间间隔后执行 function函数。系统最小时间间隔与所用的硬件平台有关,在核心里定义了常数 HZ表示一秒内最小时间间隔的数目,则 num*HZ 表示 num秒。系统计时到预定时间就调用function ,并把此子程序从定时队列里删除,可见,如果想要每隔一定时间间隔执行一次的话,就必须在 function里再一次调用 add_timer 。 function 的参数 d即为 timer里面的data项。

Page 18: 第 6 章   嵌入式 Linux 驱动程序开发

3 .内存操作函数作为系统核心的一部分,设备驱动程序在申请和释放内存时不是调用 malloc 和

free ,而代之以调用 kmalloc 和 kfree ,它们在 linux/kernel.h 中被定义为:void * kmalloc(unsigned int len, int priority);void kfree(void * obj);参数 len 为希望申请的字节数, obj 为要释放的内存指针。 priority 为分配内存

操作的优先级,即在没有足够空闲内存时如何操作,一般由取值 GFP_KERNEL解决即可。

4 .复制函数在用户程序调用 read 、 write 时,因为进程的运行状态由用户态变为核心态,

地址空间也变为核心地址空间。由于 read 、 write 中参数 buf 是指向用户程序的私有地址空间的,所以不能直接访问,必须通过下面两个系统函数来访问用户程序的私有地址空间。

#include <asm/segment.h>void memcpy_fromfs(void * to,const void * from,unsigned long n);void memcpy_tofs(void * to,const void * from,unsigned long n);memcpy_fromfs 由用户程序地址空间往核心地址空间复制, memcpy_tofs 则

反之。参数 to 为复制的目的指针, from 为源指针, n 为要复制的字节数。在设备驱动程序里,可以调用 printk 来打印一些调试信息, printk 的用法与 pri

ntf 类似。 printk打印的信息不仅出现在屏幕上,同时还记录在文件 syslog里。

Page 19: 第 6 章   嵌入式 Linux 驱动程序开发

加载和卸载驱动程序 1 .入口函数在编写模块程序时,必须提供两个函数,一个是 int init_module() ,在加载此模

块的时候自动调用,负责进行设备驱动程序的初始化工作。 init_module() 返回 0 ,表示初始化成功,返回负数表示失败,它在内核中注册一定的功能函数。在注册之后,如果有程序访问内核模块的某个功能,内核将查表获得该功能的位置,然后调用功能函数。 init_module() 的任务就是为以后调用模块的函数做准备。

另一个函数是 void cleanup_module() ,该函数在模块被卸载时调用,负责进行设备驱动程序的清除工作。这个函数的功能是取消 init_module() 所做的事情,把 init_module()函数在内核中注册的功能函数完全卸载,如果没有完全卸载,在此模块下次调用时,将会因为有重名的函数而导致调入失败。

在 2.3版本以上的 Linux 内核中,提供了一种新的方法来命名这两个函数。例如,可以定义 init_my()代替 init_module()函数,定义 exit_my()代替 cleanup_module()函数,然后在源代码文件末尾使用下面的语句:

module_init(init_my) ;module_exit(exit_my) ;这样做的好处是,每个模块都可以有自己的初始化和卸载函数的函数名,多个模

块在调试时不会有重名的问题。

Page 20: 第 6 章   嵌入式 Linux 驱动程序开发

2 .模块加载与卸载虽然模块作为内核的一部分,但并未被编译到内核中,它们被分别编译和链接

成目标文件。 Linux 中模块可以用 C语言编写,用 gcc 命令编译成模块 *.o ,在命令行里加上 -c 的参数和“ -D__KERNEL__-DMODULE”参数。然后用 depmod -a 使此模块成为可加载模块。模块用 insmod 命令加载,用 rmmod 命令来卸载,这两个命令分别调用 init_module() 和 cleanup_ module()函数,还可以用 lsmod命令来查看所有已加载的模块的状态。

insmod 命令可将编译好的模块调入内存。内核模块与系统中其他程序一样是已链接的目标文件,但不同的是它们被链接成可重定位映像。 insmod 将执行一个特权级系统调用 get_kernel_sysms()函数以找到内核的输出内容, insmod修改模块对内核符号的引用后,将再次使用特权级系统调用 create_module()函数来申请足够的物理内存空间,以保存新的模块。内核将为其分配一个新的 module 结构,以及足够的内核内存,并将新模块添加在内核模块链表的尾部,然后将新模块标记为 uninitialized 。

利用 rmmod 命令可以卸载模块。如果内核中还在使用此模块,这个模块就不能被卸载。原因是如果设备文件正被一个进程打开就卸载还在使用的内核模块,并导致对内核模块的读 /写函数所在内存区域的调用。如果幸运,没有其他代码被加载到那个内存区域,将得到一个错误提示;否则,另一个内核模块被加载到同一区域,这就意味着程序跳到内核中另一个函数的中间,结果是不可预见的。

Page 21: 第 6 章   嵌入式 Linux 驱动程序开发

6.3 LED 驱动程序设计

Page 22: 第 6 章   嵌入式 Linux 驱动程序开发

LED 接口设计 S3C2410X 提供了多达 117 个可编程的通用 I/O 端口,可以方便地输入输出各种信号。SBC-2410X目标板选用 S3C2410X微处理器,带有 4 个用户可编程 I/O 方式的 LED ,硬件原理图如图 6.2 所示,表 6.1 为 LED对应的 I/O 口。 LED 控制采用低电平有效方式,当端口电平为低时点亮 LED 指示灯,输出高电平时 LED熄灭。与 LED 相连的通用 I/O端口由表 6.3 所示的控制寄存器配置。

Page 23: 第 6 章   嵌入式 Linux 驱动程序开发
Page 24: 第 6 章   嵌入式 Linux 驱动程序开发

...#define GPIO_CTL_BASE 0x56000000 /*IO 口控制寄存器及地址 */#define bGPIO(p) __REG(GPIO_CTL_BASE + (p)) /* 寄存器地址 0X50000000+ p*/#define GPBCON bGPIO(0x10) /* 寄存器地址 0X56000010*/#define GPBDAT bGPIO(0x14) /* 寄存器地址 0X56000014*/#define GPBUP bGPIO(0x18) /* 寄存器地址 0X56000018*/...#define MAKE_GPIO_NUM(p, o) ((p << GPIO_PORT_SHIFTT) | (o << GPIO_OFS_SHIFT))#define PORTB_OFS 1#define GPIO_B7 MAKE_GPIO_NUM(PORTB_OFS, 7) /* 端口 GPB7*/#define GPIO_B8 MAKE_GPIO_NUM(PORTB_OFS, 8) /* 端口 GPB8*/#define GPIO_B9 MAKE_GPIO_NUM(PORTB_OFS, 9) /* 端口 GPB9*/#define GPIO_B10 MAKE_GPIO_NUM(PORTB_OFS, 10) /* 端口 GBP10*/...#define GPCON(x) __REG2(0x56000000, (x) * 0x10) /* 功能选择寄存器地址 */#define GPDAT(x) __REG2(0x56000004, (x) * 0x10) /* 数据寄存器地址 */#define GPUP(x) __REG2(0x56000008, (x) * 0x10) /* 上拉电阻设置寄存器地址 */...#define GPIO_OFS_SHIFT 0#define GPIO_PORT_SHIFTT 8#define GPIO_PULLUP_SHIFT 16 #define GPIO_MODE_SHIFT 24#define GPIO_OFS_MASK 0x000000ff#define GPIO_PORT_MASK 0x0000ff00#define GPIO_PULLUP_MASK 0x00ff0000#define GPIO_MODE_MASK 0xff000000#define GPIO_MODE_IN (0 << GPIO_MODE_SHIFT)#define GPIO_MODE_OUT (1 << GPIO_MODE_SHIFT)#define GPIO_MODE_ALT0 (2 << GPIO_MODE_SHIFT)#define GPIO_MODE_ALT1 (3 << GPIO_MODE_SHIFT)#define GPIO_PULLUP_EN (0 << GPIO_PULLUP_SHIFT)#define GPIO_PULLUP_DIS (1 << GPIO_PULLUP_SHIFT)...#define GRAB_MODE(x) (((x) & GPIO_MODE_MASK) >> GPIO_MODE_SHIFT)#define GRAB_PULLUP(x) (((x) & GPIO_PULLUP_MASK) >> GPIO_PULLUP_SHIFT)#define GRAB_PORT(x) (((x) & GPIO_PORT_MASK) >> GPIO_PORT_SHIFTT)#define GRAB_OFS(x) (((x) & GPIO_OFS_MASK) >> GPIO_OFS_SHIFT)...

Page 25: 第 6 章   嵌入式 Linux 驱动程序开发

/* 端口参数设置宏 */#define set_gpio_ctrl(x) \

({ GPCON(GRAB_PORT((x))) &= ~(0x3 << (GRAB_OFS((x))*2)); \ GPCON(GRAB_PORT(x)) |= (GRAB_MODE(x) << (GRAB_OFS((x))*2)); \ GPUP(GRAB_PORT((x))) &= ~(1 << GRAB_OFS((x))); \ GPUP(GRAB_PORT((x))) |= (GRAB_PULLUP((x)) << GRAB_OFS((x))); })

#define set_gpio_pullup(x) \({ GPUP(GRAB_PORT((x))) &= ~(1 << GRAB_OFS((x))); \ GPUP(GRAB_PORT((x))) |= (GRAB_PULLUP((x)) << GRAB_OFS((x))); })

#define set_gpio_pullup_user(x, v) \({ GPUP(GRAB_PORT((x))) &= ~(1 << GRAB_OFS((x))); \ GPUP(GRAB_PORT((x))) |= ((v) << GRAB_OFS((x))); })

#define set_gpio_mode(x) \({ GPCON(GRAB_PORT((x))) &= ~(0x3 << (GRAB_OFS((x))*2)); \ GPCON(GRAB_PORT((x))) |= (GRAB_MODE((x)) << (GRAB_OFS((x))*2)); })

#define set_gpio_mode_user(x, v) \({ GPCON(GRAB_PORT((x))) & = ~(0x3 << (GRAB_OFS((x))*2)); \ GPCON(GRAB_PORT((x))) |= ((v) << (GRAB_OFS((x))*2)); })

#define set_gpioA_mode(x) \({ GPCON(GRAB_PORT((x))) &= ~(0x1 << GRAB_OFS((x))); \ GPCON(GRAB_PORT((x))) |= (GRAB_MODE((x)) << GRAB_OFS((x))); })

#define read_gpio_bit(x) ((GPDAT(GRAB_PORT((x))) & (1<<GRAB_OFS((x)))) >> GRAB_OFS((x)))#define read_gpio_reg(x) (GPDAT(GRAB_PORT((x)))#define write_gpio_bit(x, v) \

({ GPDAT(GRAB_PORT((x))) &= ~(0x1 << GRAB_OFS((x))); \ GPDAT(GRAB_PORT((x))) |= ((v) << GRAB_OFS((x))); })

#define write_gpio_reg(x, v) (GPDAT(GRAB_PORT((x))) = (v))

Page 26: 第 6 章   嵌入式 Linux 驱动程序开发

LED 驱动程序代码分析 1 .系统资源和宏定义#define DEVICE_NAME "leds" /* 定义 led 设备的名字 */#define LED_MAJOR 231 /* 定义 led 设备的主设备号 */static unsigned long led_table [ ] = { /*I/O 方式 led 设备对应的硬件资源 */GPIO_B7,GPIO_B8,GPIO_B9,GPIO_B10,};

2 .入口函数模块的入口函数 leds_init() ,所做的工作是点亮 I/O 端口对应的 LED ,这些二极管可以作为

状态指示之用。 register_chrdev() 完成字符设备在系统中的注册,并建立与文件系统的并联。static int __init leds_init (void){int ret;int i;/* 在内核中注册设备 */ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops);if (ret < 0) {printk(DEVICE_NAME " can't register major number\n");return ret;}devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT,LED_ MAJOR, 0, S_IFCHR | S_IRUSR | S

_IWUSR, &matrix4_leds_fops,NULL);/* 使用宏进行端口初始化, set_gpio_ctrl 和 write_gpio_bit 均为宏定义 */for (i = 0; i < 8; i++) {set_gpio_ctrl (led_table[i] | GPIO_PULLUP_EN | GPIO_MODE_OUT);write_gpio_bit(led_table[i], 1);}printk(DEVICE_NAME " initialized\n");return 0;}

卸载模块时调用 leds_exit()函数,执行后设备处于空闲状态。static void __exit leds_exit (void){devfs_unregister(devfs_handle);unregister_chrdev(LED_MAJOR, DEVICE_NAME);}

Page 27: 第 6 章   嵌入式 Linux 驱动程序开发

3 .字符设备定义static devfs_handle_t devfs_handle;4 . ioctlioctl 入口,执行读、写之外的操作,通过参数 cmd 设定命令参数。static int leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned longarg){switch(cmd) {case 0:case 1:if (arg > 4) {return -EINVAL;}write_gpio_bit(led_table[arg], !cmd);default:return -EINVAL;}}5 .文件系统接口定义static struct file_operations leds_fops = {owner: THIS_MODULE,ioctl: leds_ioctl,};6 .模块化module_init(leds_init);module_exit(leds_exit);用 insmod 命令加载模块时,调用 module_init() ;用 rmmod 命令来卸载模块时,调用 module_exit()函数。

Page 28: 第 6 章   嵌入式 Linux 驱动程序开发

加载运行 LED 驱动程序 1 .应用程序设计#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/ioctl.h>int main(int argc, char **argv){

int on;int led_no;int fd;if (argc != 3 || sscanf(argv[1], "%d", &led_no) != 1 || sscanf(argv[2],"%d", &on) != 1 ||

on < 0 || on > 1 || led_no < 0 || led_no > 3) {fprintf(stderr, "Usage: ledtest led_no 0|1\n");exit(1);

}

if (fd < 0) {perror("open device leds");exit(1);

}ioctl(fd,on,led_no);close(fd);return 0;

}

该程序首先读取命令行的参数输入,其中参数 argv[1]赋值给 led_no ,表示发光二极管的序号; argv[2]赋值给 on 。 led_no 的取值范围是 1~3 , on 取值为 0 或 1 , 0 表示熄灭 LED , 1 表示点亮 LED 。参数输入后通过 fd = open("/dev/leds", 0)打开设备文件,在保证参数输入正确和设备

文件正确打开后,通过语句 ioctl(fd, on, led_no) 实现系统调用 ioctl ,并通过输入的参数控制 LED 。在程序的最后关闭设备句柄。

Page 29: 第 6 章   嵌入式 Linux 驱动程序开发

2 .加载驱动首先编写Makefile 文件,如下所示:INCLUDE = /usr/linux/includeEXTRA_CFLAGS = -D_ KERNEL_ -DMODULE –I $ (INCLUDE ) –02 –Wall –Oall: leds.o ledtestleds.o: leds.c

arm-linux-gcc $(CFLAGS )$( EXTRA_CFLAGS) –c leds.c –o leds.oledtest: ledtest.c

arm-linux-gcc –g led.c –o ledtestclean:

rm –rf leds.orm –rf ledtest

对Makefile 文件执行 make 命令后,可以生成驱动模块 leds 和测试程序 ledtest了。如果不想编写Makefile 文件,也可以使用手动输入命令的方式编译驱动模块:

$arm-linux-gcc -D_ KERNEL_ -DMODULE –I $ (INCLUDE ) –02 –Wall –O –c leds.c –o leds.o

以上命令将生成 leds.o 文件,将该文件复制到目标板的 /lib目录下,使用以下命令安装 leds模块:

$insmod /lib/leds.o删除该模块的命令是:$rmmod leds.o应用程序编译正确后如输入:$ledtest则提示: Usage:ledtest led_no 0|1若输入: $ledtest 2 1则点亮 LED3 。

Page 30: 第 6 章   嵌入式 Linux 驱动程序开发

6.4 按键驱动程序设计

Page 31: 第 6 章   嵌入式 Linux 驱动程序开发

S3C2410X 中断控制器

Page 32: 第 6 章   嵌入式 Linux 驱动程序开发
Page 33: 第 6 章   嵌入式 Linux 驱动程序开发
Page 34: 第 6 章   嵌入式 Linux 驱动程序开发

S3C2410X 中断接口函数 include <linux/config.h> /* Interrupt Controller */#define IRQ_EINT0 0 /* External interrupt 0 */#define IRQ_EINT1 1 /* External interrupt 1 */#define IRQ_EINT2 2 /* External interrupt 2 */#define IRQ_EINT3 3 /* External interrupt 3 */#define IRQ_EINT4_7 4 /* External interrupt 4 ~ 7 */#define IRQ_EINT8_23 5 /* External interrupt 8 ~ 23 */#define IRQ_RESERVED66 /* Reserved for future use */#define IRQ_BAT_FLT 7#define IRQ_TICK 8 /* RTC time tick interrupt */#define IRQ_WDT 9 /* Watch-Dog timer interrupt */#define IRQ_TIMER0 10 /* Timer 0 interrupt */#define IRQ_TIMER1 11 /* Timer 1 interrupt */#define IRQ_TIMER2 12 /* Timer 2 interrupt */#define IRQ_TIMER3 13 /* Timer 3 interrupt */#define IRQ_TIMER4 14 /* Timer 4 interrupt */#define IRQ_UART2 15 /* UART 2 interrupt */#define IRQ_LCD 16 /* reserved for future use */#define IRQ_DMA0 17 /* DMA channel 0 interrupt */#define IRQ_DMA1 18 /* DMA channel 1 interrupt */#define IRQ_DMA2 19 /* DMA channel 2 interrupt */#define IRQ_DMA3 20 /* DMA channel 3 interrupt */#define IRQ_SDI 21 /* SD Interface interrupt */#define IRQ_SPI0 22 /* SPI interrupt */#define IRQ_UART1 23 /* UART1 receive interrupt */#define IRQ_RESERVED24 24#define IRQ_USBD 25 /* USB device interrupt */#define IRQ_USBH 26 /* USB host interrupt */#define IRQ_IIC 27 /* IIC interrupt */#define IRQ_UART0 28 /* UART0 transmit interrupt */#define IRQ_SPI1 29 /* UART1 transmit interrupt */#define IRQ_RTC 30 /* RTC alarm interrupt */#define IRQ_ADCTC 31 /* ADC EOC interrupt */#define NORMAL_IRQ_OFFSET 32

Page 35: 第 6 章   嵌入式 Linux 驱动程序开发

#/* External Interrupt */#define IRQ_EINT4 (0 +NORMAL_IRQ_OFFSET)#define IRQ_EINT5 (1 +NORMAL_IRQ_OFFSET)#define IRQ_EINT6 (2 +NORMAL_IRQ_OFFSET)#define IRQ_EINT7 (3 +NORMAL_IRQ_OFFSET)#define IRQ_EINT8 (4 +NORMAL_IRQ_OFFSET)#define IRQ_EINT9 (5 +NORMAL_IRQ_OFFSET)#define IRQ_EINT10 (6 +NORMAL_IRQ_OFFSET)#define IRQ_EINT11 (7 +NORMAL_IRQ_OFFSET)#define IRQ_EINT12 (8 +NORMAL_IRQ_OFFSET)#define IRQ_EINT13 (9 +NORMAL_IRQ_OFFSET) #define IRQ_EINT14 (10 +NORMAL_IRQ_OFFSET)#define IRQ_EINT15 (11 +NORMAL_IRQ_OFFSET)#define IRQ_EINT16 (12 +NORMAL_IRQ_OFFSET) #define IRQ_EINT17 (13 +NORMAL_IRQ_OFFSET) #define IRQ_EINT18 (14 +NORMAL_IRQ_OFFSET) #define IRQ_EINT19 (15 +NORMAL_IRQ_OFFSET) #define IRQ_EINT20 (16 +NORMAL_IRQ_OFFSET) #define IRQ_EINT21 (17 +NORMAL_IRQ_OFFSET) #define IRQ_EINT22 (18 +NORMAL_IRQ_OFFSET) #define IRQ_EINT23 (19 +NORMAL_IRQ_OFFSET) /* 51 */#define SHIFT_EINT4_7 IRQ_EINT4_7#define SHIFT_EINT8_23 IRQ_EINT8_23#define EXT_IRQ_OFFSET (20 +NORMAL_IRQ_OFFSET)/* sub Interrupt */#define IRQ_RXD0 (0 +EXT_IRQ_OFFSET)#define IRQ_TXD0 (1 +EXT_IRQ_OFFSET)#define IRQ_ERR0 (2 +EXT_IRQ_OFFSET)#define IRQ_RXD1 (3 +EXT_IRQ_OFFSET)#define IRQ_TXD1 (4 +EXT_IRQ_OFFSET) #define IRQ_ERR1 (5 +EXT_IRQ_OFFSET)#define IRQ_RXD2 (6 +EXT_IRQ_OFFSET)#define IRQ_TXD2 (7 +EXT_IRQ_OFFSET)#define IRQ_ERR2 (8 +EXT_IRQ_OFFSET) #define IRQ_TC (9 +EXT_IRQ_OFFSET) #define IRQ_ADC_DONE (10 +EXT_IRQ_OFFSET) /* 62 */#define SHIFT_UART0 IRQ_UART0#define SHIFT_UART1 IRQ_UART1#define SHIFT_UART2 IRQ_UART2#define SHIFT_ADCTC IRQ_ADCTC#define SUB_IRQ_OFFSET (11 +EXT_IRQ_OFFSET)#define IRQ_UNKNOWN SUB_IRQ_OFFSET#define NR_IRQS SUB_IRQ_OFFSET#define OS_TIMER IRQ_TIMER4

Page 36: 第 6 章   嵌入式 Linux 驱动程序开发

1 .宏定义直接使用写 1 的方式进行清中断操作。#define ClearPending(x) { \

SRCPND = (1 << (x)); \ INTPND = (1 << (x)); \}

#define EINT_OFFSET(x) ((x) - NORMAL_IRQ_OFFSET + 4)#define SUBIRQ_OFFSET(x) ((x) - EXT_IRQ_OFFSET)#define EXTINT_MASK 0x7#if 0#define EXTINT_OFFSET 0x4#define EXTINT_MASK 0x7int set_EXT_IRQ_mode(int irq, int edge)EXPORT_SYMBOL(set_EXT_IRQ_mode);#endif

其中 set_EXT_IRQ_mode(int irq, int edge)函数的 irq 用来设定中断号, edge 用来设定采用的边沿触发方式。

2 .禁用和使能中断 常规中断禁用并清中断源: static void s3c2410_mask_ack_irq(unsigned int irq)

常规禁用中断: static void s3c2410_mask_irq(unsigned int irq)

常规使能中断: static void s3c2410_unmask_irq(unsigned int irq)

外部中断禁用并清中断源: static void EINT4_23mask_ack_irq(unsigned int irq)

外部禁用中断: static void EINT4_23mask_irq(unsigned int irq)

外部使能中断: static void EINT4_23unmask_irq(unsigned int irq)

子中断禁用并清中断源: static void SUB_mask_ack_irq(unsigned int irq)

子中断禁用: static void SUB_mask_irq(unsigned int irq)

子中断使能: static void SUB_unmask_irq(unsigned int irq)

带子中断处理的函数,需要清除子 mask 、 pend 、 INTMASK 和 INTPND 中的对应位。

Page 37: 第 6 章   嵌入式 Linux 驱动程序开发

3 .获取中断号操作获得可以访问中断处理函数的 irq 号码( 0-62 )。 获得子中断号码:inline unsigned int get_subIRQ(int irq, int begin, int end, int fail_irq)其中 begin 和 end 为起止中断号, fail_irq 为查询失误后返回值。 获得外部中断号码:inline unsigned int get_extIRQ(int irq, int begin, int end, int fail_irq) 获得 irq 号:unsigned int fixup_irq(int irq) 获得 irq 号码,在 extint 中使用:static int inline fixup_irq_num(int irq)4 .中断初始化函数在使用 S3C2410X微处理器时,可以采用初始化函数:void __init s3c2410_init_irq(void)在完成对处理器中断的初始化后, s3c2410_init_irq 中没有将外部中断置为有效,

实际上也不可能置为有效,这需要在用户驱动程序中设置。设置时,可以使用外部中断初始化函数:

int set_external_irq(int irq, int edge, int pullup)该函数中引用了端口设置函数:static void inline set_gpios(int irq, int pullup)该函数通过对 GPF 和 GPG 相关寄存器的操作,实现对 EINT0~23 的端口模式

的设置。由于使用 gpio ,所以需要制定指为 irq ,且上拉电阻无效。上拉电阻一般在中断设置时无效,端口使用时有效。

Page 38: 第 6 章   嵌入式 Linux 驱动程序开发

键盘驱动代码设计在键盘接口电路中,使用 GPIO 端口,复用为中断和数据方式,键盘按下或者抬起都将导致中断,并在中断处理过程中转换为数据端口方式,读入数据,然后再改为中断方式,等待中断。部分程序代码如下。1 .系统资源和宏定义#define DEVICE_NAME "buttons" /* 定义按键设备名 */#define BUTTON_MAJOR 232 /* 定义按键主设备号 *//* 定义按键所使用的 CPU 资源 */static struct key_info {int irq_no;unsigned int gpio_port;int key_no;} key_info_tab[4] = { { IRQ_EINT1, GPIO_F1, 1 },{ IRQ_EINT2, GPIO_F2, 2 },{ IRQ_EINT3, GPIO_F3, 3 },{ IRQ_EINT7, GPIO_F7, 4 },};

Page 39: 第 6 章   嵌入式 Linux 驱动程序开发

2 .中断服务程序static void irq(int irq, void *dev_id, struct pt_regs *reg){struct key_info *k;int i;int found = 0;int up;int flags;/* 使用 irq 号码判断是否是某个键盘,因为中断程序被 4 个按键中断共用 */for (i = 0; i < sizeof key_info_tab / sizeof key_info_tab[1]; i++) {k = key_info_tab + i;if (k->irq_no == irq) {found = 1;break;}}if (!found) {printk("bad irq %d in button\n", irq);return;}save_flags(flags);/*保存当前中断状态 */cli();/置位 cpsr ,禁用中断 *//*复用中断端口,从端口读取电平高低值 */set_gpio_mode_user(k->gpio_port, GPIO_MODE_IN);up = read_gpio_bit(k->gpio_port);/*复用中断端口,从端口读取电平高低值 */set_external_irq(k->irq_no, EXT_BOTH_EDGES, GPIO_PULLUP_DIS);restore_flags(flags);/*恢复中断状态 *//*保存从中断读出的值 */if (up) {key_value = k->key_no + 0x80;} else {key_value = k->key_no;}ready = 1;/*置位全局状态 */wake_up_interruptible(&buttons_wait);/* 等待任务唤醒, select调用的进程 */}

Page 40: 第 6 章   嵌入式 Linux 驱动程序开发

申请系统中断,中断方式为双边触发,即在上升沿和下降沿均发生中断:static int request_irqs(void){struct key_info *k;int i;for (i = 0; i < sizeof key_info_tab / sizeof key_info_tab[1]; i++) {k = key_info_tab + i;/* 使用外部中断,单独设置触发方式 */set_external_irq(k->irq_no, EXT_BOTH_EDGES, GPIO_PULLUP_DIS);/*申请中断资源 */if (request_irq(k->irq_no, &buttons_irq, SA_INTERRUPT,DEVICE_NAME,&buttons_irq)){return -1;}}return 0;}

释放中断:static void free_irqs(void){struct key_info *k;int i;for (i = 0; i < sizeof key_info_tab / sizeof key_info_tab[1]; i++) {k = key_info_tab + i;free_irq(k->irq_no, buttons_irq);}}

Page 41: 第 6 章   嵌入式 Linux 驱动程序开发

3 .文件系统的读函数实现static int buttons_read(struct file * file, char * buffer, size_t count, loff_t *ppos){static int key;int flags;int repeat;if (!ready) return -EAGAIN;if (count != sizeof key_value) return -EINVAL;save_flags(flags);if (key != key_value) {key = key_value;repeat = 0;} else {repeat = 1;}restore_flags(flags);if (repeat) {return -EAGAIN;}/* 使用 copy_to_user 把键值送到用户空间 */copy_to_user(buffer, &key, sizeof key);ready = 0;return sizeof key_value;}

4 . ioctl函数static int buttons_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg){switch(cmd) {default:return -EINVAL;}}

Page 42: 第 6 章   嵌入式 Linux 驱动程序开发

5 .文件系统接口和字符设备定义static struct file_operations buttons_fops = {owner: THIS_MODULE,ioctl: buttons_ioctl,poll: buttons_select,read: buttons_read,};static devfs_handle_t devfs_handle;

6 .系统入口函数/*按键初始化 */static int __init buttons_init(void){int ret;ready = 0;/*注册按键设备 */ret = register_chrdev(BUTTON_MAJOR, DEVICE_NAME, &buttons_fops);if (ret < 0) {printk(DEVICE_NAME " can't register major number\n");return ret;}ret = request_irqs();/*申请中断资源 */if (ret) {unregister_chrdev(BUTTON_MAJOR, DEVICE_NAME);printk(DEVICE_NAME " can't request irqs\n");return ret;}/*注册 devfs ,从而可以使用 devfs 文件系统 */devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT,BUTTON_MAJOR, 0, S_IFCHR | S_IRUSR | S_IWUSR,&buttons_fops, NULL);return 0;}

Page 43: 第 6 章   嵌入式 Linux 驱动程序开发

static void __exit buttons_exit(void){devfs_unregister(devfs_handle);free_irqs();unregister_chrdev(BUTTON_MAJOR, DEVICE_NAME);}

完整的程序位于 buttons/buttons.c 中,该程序参考了友善之臂公司提供的按键测试程序 matrix5-buttons.c 。

使用手工输入命令的方式编译 buttons 驱动模块:$arm-linux-gcc -D__KERNEL__ -I /kernel/include -DKBUILD_BASENAME=buttons –DMODULE –c –o butto

ns.o buttons.c以上命令将生成 buttons.o 文件,把该文件复制到目标板的 /lib 目录下,

使用以下命令安装 buttons模块:$insmod /lib/buttons.o卸载该模块的命令是:$rmmod buttons.o

Page 44: 第 6 章   嵌入式 Linux 驱动程序开发

按键测试程序的实现

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/ioctl.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <sys/select.h>#include <sys/time.h>#include <errno.h>int main(void){int buttons_fd;int key_value;buttons_fd = open("/dev/buttons", 0);if (buttons_fd < 0) {perror("open device buttons");exit(1);}

for (;;) {fd_set rds;int ret;FD_ZERO(&rds);FD_SET(buttons_fd, &rds);ret = select(buttons_fd + 1, &rds, NULL, NULL, NULL);if (ret < 0) {perror("select");exit(1);}if (ret == 0) {printf("Timeout.\n");} else if (FD_ISSET(buttons_fd, &rds)) {int ret = read(buttons_fd, &key_value, sizeof key_value);if (ret != sizeof key_value) {if (errno != EAGAIN) perror("read buttons\n");continue;} else {printf("buttons_value: %d\n", key_value);}}}close(buttons_fd);return 0;}

有了按键驱动程序,就可以在应用程序中编写驱动调用函数,然后利用文件操作实现对按键的各种操作了。完整的应用程序源代码清单如下:

Page 45: 第 6 章   嵌入式 Linux 驱动程序开发

1 .文件描述符集 fd_set在 linux/types.h 中存在对文件描述符集 fd_set 的定义: typedef __kernel_fd_set fd_set;而在 linux/posix_types 中存在结构的声明:typedef struct {unsigned long fds_bits [__FDSET_LONGS];} __kernel_fd_setfd_set 实际上是 long 类型的数组,每一个数组元素都能与一个打开的文件句柄建立联系,不管

是 Socket句柄,还是其他文件或设备句柄。调用 select() 时,由内核根据 IO 状态修改 fd_set 的内容,由此来通知执行了 select() 的进程哪一个文件可读。

2 . select() 系统调用函数select() 系统调用可以使进程检测同时等待的多个 I/O 设备,当没有设备准备好时, select()阻

塞,其中任一设备准备好时, select() 就返回。 select() 的调用形式为:int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *ti

meout); 参数 maxfd 指定文件描述符集中要被检测的比特数,至少要比待检测的最大文件描述符大 1 。 参数 readfds 指定被读监控的文件描述符集。 参数 writefds 指定被写监控的文件描述符集。 参数 exceptfds 指定被例外条件监控的文件描述符集。 参数 timeout 起了定时器的作用,到了指定的时间,无论是否有设备准备好,都返回调用。 t

imeout 取不同的值,该调用就表现不同的性质。 timeout 为 0 ,调用立即返回; timeout 为 NULL ,select() 一被调用就阻塞,直到知道有文件描述符就绪; timeout 为正整数,就是一般的定时器。

select调用返回时,除了那些已经就绪的描述符外, select 将清除 readfds 、 writefds 和 exceptfds 中的所有没有就绪的描述符。 select 的返回值有如下情况:

正常情况下返回就绪的文件描述符个数; 经过 timeout 时长后仍无设备准备好,返回值为 0 ; 如果 select被某个信号中断,它将返回 1 并设置 errno 为 EINTR ; 如果出错,返回 1 并设置相应的 errno 。

Page 46: 第 6 章   嵌入式 Linux 驱动程序开发

3 .系统提供了 4 个宏对描述符集进行操作:void FD_SET(int fd, fd_set *fdset); void FD_CLR(int fd, fd_set *fdset); void FD_ISSET(int fd, fd_set *fdset); void FD_ZERO(fd_set *fdset); 宏 FD_SET 设置文件描述符集 fdset 中对应于文件描述符 fd 的位(设置为 1 );宏 FD_CLR清除文件描述符集 fdset 中对应于文件描述符 fd 的位(设置为 0 );宏 FD_ZERO清除文件描述符集 fdset 中的所有位(即把所有位都设置为 0 )。使用这 3 个宏在调用 select 前设置描述符屏蔽位,在调用 select 后使用 FD_ISSET 来检测文件

描述符集 fdset 中对应于文件描述符 fd 的位是否被设置。过去,描述符集用一个整数位屏蔽码来实现,但是这种实现对于多于 32 个的文件描述符将无法

工作。描述符集现在通常用整数数组中的位域表示,数组元素的每一位对应一个文件描述符。宏 FD_SET 设置整数数组中对应于 fd 文件描述符的位为 1 ;宏 FD_CLR 设置整数数组中对应于 fd 文件描述符的位为 0 ;宏 FD_ZERO 设置整数数组中的所有位都为 0 。4 . read/write函数定义:ssize_t read(int fd, void *buffer,size_t count);ssize_t write(int fd, const void *buffer,size_t count);fd 是我们要进行读写操作的文件描述符, buffer 是我们要写入文件内容或读出文件内容的内存存

放地址, count 是我们要读写的字节数。对于普通的文件 read从指定的文件( fd )中读取 count 个字节到 buffer 缓冲区中(注意:必须

提供一个足够大的缓冲区),同时返回 count 。如果 read读到了文件的结尾或者被一个信号中断,返回值会小于 count 。如果是由信号中断引起

返回,而且没有返回数据, read会返回 1 ,且设置 errno 为 EINTR 。当程序读到文件结尾的时候,read会返回 0 。 write从 buffer 中写 count 个字节到文件 fd 中,成功时返回实际所写的字节数。