字符设备驱动程序——点亮、熄灭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。
硬件连接方式如下图:
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