字符设备驱动程序——点亮、熄灭LED操作

字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

每一个字符设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备。

Linux操作系统将所有的设备看成文件,以操作文件的方式访问设备。应用程序不能直接操作硬件,而是使用统一的接口函数调用硬件的驱动程序。这组接口被称为系统调用,在库函数中定义。可以在glibc的fcntl.h、unistd.h、sys/ioctl.h等文件中看到如下的定义,这个些文件也可以在交叉编译工具链的/usr/local/arm/3.4.1/include目录下找到。

extern int open (__const char *__file, int __oflag, ...) __nonnull ((1));

extern ssize_t read (int __fd, void *__buf, size_t __nbytes);

extern ssize_t write (int __fd, __const void *__buf, size_t __n);

extern int ioctl (int __fd, unsigned long int __request, ...) __THROW;

.......

对于上述每个系统调用,驱动程序中都有一个与之对应的函数。对于字符设备驱动程序,这些函数集合在一个file_operations类型的数据结构中。file_operations结构在Linux内核的include/linux/fs.h文件中定义。

struct file_operations {

struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, 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);

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *, fl_owner_t id);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, struct dentry *, int datasync);

int (*aio_fsync) (struct kiocb *, int datasync);

int (*fasync) (int, struct file *, int);

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);

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);

int (*check_flags)(int);

int (*dir_notify)(struct file *filp, unsigned long arg);

int (*flock) (struct file *, int, struct file_lock *);

ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);

};

当应用程序使用open函数打开某个设备时,设备驱动程序的file_operations结构中的open成员就会被调用;当应用程序使用read、write、ioctl等函数读写、控制设备时,驱动程序的file_operations结构中的相应成员(read、write、ioctl等)就会被调用。从这个角度来说,编写字符设备驱动程序就是为具体硬件的file_operations结构编写各个函数(并不需要全部实现file_operations结构中的成员)。

那么,当应用程序通过open、read、write等系统调用访问某个设备文件时,Linux系统怎么知道去调用那个驱动程序的file_operations结构中的open、read、write等成员呢?

(1)、设备文件主/次设备号

设备文件分为字符设备、块设备,比如PC机上的串口属于字符设备,硬盘属于块设备。在PC机上运行命令“ls /dev/ttyS0 /dev/hdal -l”可以看到:

brw-rw---- 1 root disk   3, 1  5月 21 08:29 /dev/hdal

crw-rw---- 1 root uucp  4, 64  5月 21 08:29 /dev/ttyS0

“brw-rw----”中的“b”表示/dev/hdal是个块设备,他的主设备号是3,次设备号是1;“crw-rw----”中的“c”表示/dev/ttyS0是个块设备,它的主设备号位4,次设备号为64。

(2)、模块初始化时,将主设备号与file_operations结构一起向内核注册

驱动程序有一个初始化函数,在安装驱动程序时会调用它。在初始化函数中,会将驱动程序的file_operations结构连同主设备号一起向内核进行注册。对于字符设备使用如下以下函数进行注册:

extern int register_chrdev(unsigned int, const char *,const struct file_operations *);

这样,应用程序操作设备文件时,Linux系统就将会根据设备文件的类型(是字符设备还是块设备)、主设备号找到在内核中注册的file_operations结构(对于块设备位block_device_operations结构),次设备号供驱动程序自身用来分辨它是同类设备中的第几个。

编写字符驱动程序的过程大概如下。

(1)、编写驱动程序初始化函数

进行必要的初始化,包括硬件初始化、向内核注册驱动程序等。

(2)、构造file_operations结构中要用到的各个成员函数

实际的驱动程序当然比上述两个步骤复杂,但这两个步骤已经可以让我们编写比较简单的驱动程序,比如LED控制。其他比较高级的技术,比如中断、select机制、fasync异步通知机制,将在其他章节的例子中介绍。

LED驱动程序源码分析:

本节以一个简单的LED驱动程序作为例子,让读者初步了解驱动程序的开发。

本例子的开发板使用引脚GPF4~6外接3个LED,它们的操作方法以前已经做了详细的说明。

(1)、引脚功能为输出;

(2)、要点亮LED,令引脚输出0;

(3)、要熄灭LED,令引脚输出1。

硬件连接方式如下图:

字符设备驱动程序——点亮、熄灭LED操作

1、LED驱动程序代码分析

下面按照函数调用的顺序进行讲解。

模块的初始化函数和卸载函数如下:

/*

* 执行“insmod first_drv.ko”命令时就会调用这个函数

*/

static int __init first_drv_init(void)

{

/* 注册字符设备驱动程序

 * 参数为主设备号、设备名字、file_operations结构;本例子中写0,

 * 内核就会自动分配一个主设备号,使用major变量存储。

 * 这样,主设备号和具体的file_operations结构就联系起来了

 * 操作主设备号为major的设备文件时,就会调用first_drv_fops中的相关成员函数

*/

99 : major = register_chrdev( 0, "first_drv", &first_drv_fops);

 

/* 创建一个struct class的指针

 * 第一个参数默认“THIS_MODULE”

 * 第二个参数和register_chrdev()函数中的驱动设备名字相同

*/

firstdrv_class = class_create(THIS_MODULE, "first_drv");

/* 创建/dev/xxx设备

 * 第一个参数为上面创建的struct class类型的变量

 * 第二个参数为NULL,第三个参数为MKDEV(major, 0)

 * 第四个参数为NULL,第五个参数为设备名字

*/

firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xxx");

 

/* 将物理端口进行映射

*/

GPFCON = (volatile unsigned long *)ioremap(0x56000050, 16);

GPFDATA = GPFCON+1;

return 0;

}

 

/* 执行rmmod first_drv命令时就会调用这个函数

 *

*/

static void __exit first_drv_exit(void)

{

/* 卸载驱动程序 */

unregister_chrdev(major, "first_drv");

/* 卸载/dev/目录下的设备 */

class_device_unregister(firstdrv_class_dev);

/* 销毁class_create()创建的类 */

class_destroy(firstdrv_class);

/* 取消端口映射 */

iounmap(GPFCON);

}

/* 这两行指定驱动程序的初始化和卸载函数 */

119 : module_init(first_drv_init);

120 : module_exit(first_drv_exit);

/* 驱动模块的许可证声明 */

MODULE_LICENSE("GPL");

第119、120两行用来指明装载、卸载模块时所调用的函数。也可以不使用这两行,但是需要将这两个函数的名字改为init_module、cleanup_module。

执行“insmod first_drv.ko”命令时就会调用first_drv_init函数,这个函数核心的代码只有第99行。它调用register_chrdev函数向内核注册驱动程序;内核会自动分配主设备号,并保存在major变量中,将主设备号位major与file_operations结构first_drv_fops联系起来。以后应用程序操作主设备号位major的设备文件时,比如open、read、write、ioctl,first_drv_fops中的相应成员函数就会被调用。但是,first_drv_fops中并不需要全部实现这些函数,用到哪个就实现哪个。

执行“rmmod first_drv”命令时就会调用first_drv_exit函数,它进而调用unregister_chrdev函数卸载驱动程序,它的功能与register_chrdev函数相反。

first_drv_init、first_drv_exit函数前的“__init”、“__exit”只有在将驱动程序静态链接进内核时才有意义。前者表示first_drv_init函数的代码被放在“.init.text”段中,这个段在使用一次后被释放(这可以节省内存);后者表示first_drv_exit函数的代码段被放在“.exit.data”段中,在连接内核时这个段没有使用,因为不能卸载静态链接的驱动程序。

下面看看first_drv_fops结构的组成。

/* 这个结构是字符设备驱动程序的核心

 * 当应用程序操作设备文件时所调用的open、read、write等函数

 * 最终会调用这个结构中的对应函数

*/

static struct file_operations first_drv_fops = {

   81 .owner = THIS_MODULE,/* 这是一个宏,指向编译模块时自动创建的__this_module变量 */

    .open =  first_drv_open,     

    .write = first_drv_write,    

};

第81行的宏THIS_MODULE在include/linux/module.h中定义如下,__this_module变量在编译模块时自动创建,无需关注这点。

# define THIS_MODULE (&__this_module)

file_operations类型的first_drv_fops结构是驱动中最重要的数据结构,编写字符设备驱动程序的主要工作也是填充其中的各个成员。比如本驱动程序中用到open、write成员被设为first_drv_open、first_drv_write函数,前者用来初始化LED所用的GPIO引脚,后者用来根据用户传入的参数设置GPIO的输出电平。

first_drv_open函数的代码如下:

/* 应用程序对设备文件/dev/xxx执行open()时,

* 就会调用first_drv_open 函数

*/

static int first_drv_open(struct inode *inode, struct file *file)

{

/* 设置GPIO引脚的功能,本驱动中LED所涉及的GPIO引脚设为输出功能 */

/*配置GPF4、5、6为输出*/

*GPFCON  &=  ~( (0X3<<(4*2)) | (0X3<<(5*2)) | (0X3<<(6*2))); //首先清零

*GPFCON  |=  ( (0X1<<(4*2)) | (0X1<<(5*2)) | (0X1<<(6*2)));  //后置1

return 0;

}

在应用程序执行“open(“/dev/xxx”)”系统调用时,first_drv_open函数将被调用。它用来将LED所涉及的GPIO引脚设为输出功能。不在模块的初始化函数中进行这些设置的原因是:虽然加载了模块,但是这个模块却不一定会被用到,就是说这些引脚不一定用于这些用途,它们可能在其他模块中另作他用。所以,在使用时才去设置它,我们把对引脚的初始化放在open操作中。

first_drv_write函数的代码如下:

/* 应用程序对设备文件/dev/xxx执行write()函数时

* 就会调用first_drv_write函数

*/

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)

{

int val;

/* 拷贝从应用程序传递过来的值到val变量中 */

copy_from_user(&val, buf, count);

if(val == 1){

//点灯

*GPFDATA &= ~((0X1<<(4)) | (0X1<<(5)) | (0X1<<(6)));

}else{

//灭灯

*GPFDATA |= ((0X1<<(4)) | (0X1<<(5)) | (0X1<<(6)));

}

return 0;

}

应用程序执行系统调用write()时,first_drv_write函数将被调用,copy_from_user(&val, buf, count);作用是将用户传递的输出取出,根据用户传递的值来设置LED灯的亮灭。

注意:应用程序执行的open、write等系统调用,它们的参数和驱动程序中相应函数的参数不是一一对应的,其中经过了内核文件系统层的转换。

系统调用的原型如下:

int open(const char *pathname, int flags);

ssize_t write(int fd, const void *buf, size_t count);

int ioctl(int fd, int request, ...);

ssize_t read(int fd, void *buf, size_t count);

file_operations结构中的成员如下:

int (*open) (struct inode *, struct file *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

可以看到,这些参数有很大一部分非常相似。

(1)、系统调用open传入的参数已经被内核文件系统层处理了,在驱动程序中看不出原来的参数了。

(2)、系统调用ioctl的参数个数可变,一般最多传入3个:后面两个参数与file_operations结构中ioctl成员的后两个参数对应。

(3)、系统调用read传入的buf、count参数,对应file_operations结构中read成员的buf、count参数。而参数offp表示用户在文件中进行存取操作的位置,当执行完读写操作后由驱动程序调用。

(4)、系统调用write与file_operations结构中write成员的参数关系,与第(3)点相似。

驱动程序的最后,有如下描述信息,它们不是必须的。

/* 描述驱动程序的一些信息,不是必须的 */

MODULE_AUTHOR(“http:\\www.100ask.net”); //驱动程序作者

MODULE_DESCRIPTION(“S3C2410 LED Driver”); //一些描述信息

MODULE_LICENSE(“GPL”); //遵循的协议

驱动程序的编译:

在次文件的同一目录下,制作Makefile文件,文件内容如下:

KERN_DIR = /work/linux/linux-2.6.22.6  #对应内核的所在目录

all:

make -C $(KERN_DIR) M=`pwd` modules

clean:    #执行make clean时删除生成的文件

make -C $(KERN_DIR) M=`pwd` modules clean

rm -rf modules.order

obj-m += first_drv.o  #执行make命令生成的模块名字为“first_drv.o”

驱动程序测试:

首先要编译测试驱动程序firstdrv_test.c,它的代码很简单,关键部分如下:

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdio.h>

/*

* firstdrvtest

*/

int main(int argc, char **argv)

{

int fd;

int val = 1;

fd = open("/dev/xxx", O_RDWR); //打开设备

if (fd < 0)

{

printf("can't open!\n");

}

if(argc != 2){

printf("Usage:\n");

printf("%s <on | off>\n",argv[0]);

return 0;

}

if(strcmp(argv[1], "on") == 0){  //判断用户运行程序时,

                //添加的参数时on还是off

val = 1;  //写入1,既点亮LED

}else{

val = 0;  //写入0,既熄灭LED

}

write(fd, &val, 4); //写入数据

return 0;

}

其中的open、write函数最终会调用驱动程序中的first_drv_open、first_drv_write函数。

现在就可以参照firstdrv_test.c的使用说明,(直接运行firstdrv_test命令即可看到)操作LED了,以下两条命令点亮、熄灭LED。

./firstdrv_test on

./firstdrv_test off

 

转载于:https://my.oschina.net/cht2000/blog/906223