1、字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
2、块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。
主设备号和次设备号(二者一起为设备号):
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
驱动程序原理图:
那么对于刚接触驱动的我们来说如何快速编写一个驱动程序呢?
最好也是最快的方法是参考内核源代码中的demo。例如现在,我想编写我们的第一个字符驱动程序,那么我们可以看看别人是怎么实现的,在内核driver目录下找到led的驱动程序,参考别人是如何实现。还有就是厂家的参考demo。这是我们最快的学习方式。和STM32学习固件库函数一样的道理。
先写出两个函数模型,打开(open)和写(write)函数:
static int first_drv_open(struct inode *inode, struct file *file) { return 0; } static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos) { return 0; }
然后是要告诉内核有这两个函数,怎样告诉内核呢?通过定义下面这样结构:
/* * NOTE: * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl * can be called without the big kernel lock held in all filesystems. */ 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); }
然后通过一个函数告诉内核:
那么谁来调用上面这个函数呢?调用上面这个函数的函数就叫做驱动入口函数(这里是first_drv_init):
入口函数需要区分是哪个驱动,所以需要修饰一下,怎么修饰呢?就是调用一个函数:
完整的myled.c函数如下:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #include <asm/uaccess.h> #include <asm/irq.h> #include <asm/io.h> #include <asm/arch/regs-gpio.h> #include <asm/hardware.h> static int first_drv_open(struct inode *inode, struct file *file) { printk("first_drv_open...\r\n"); return 0; } static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos) { printk("first_drv_write...\r\n"); return 0; } /* 这个结构是字符设备驱动程序的核心 * 当应用程序操作设备文件时所调用的open、read、write等函数, * 最终会调用这个结构中指定的对应函数 */ static struct file_operations first_drv_fops = { .owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */ .open = first_drv_open, .write = first_drv_write, }; int fisrt_drv_init(void) { register_chrdev(111, "first_drv", &first_drv_fops); return 0; } void fisrt_drv_exit(void) { unregister_chrdev(111, "first_drv"); } module_init(fisrt_drv_init); module_exit(fisrt_drv_init); MODULE_AUTHOR("http://www.100ask.net"); MODULE_VERSION("0.1.0"); MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver"); MODULE_LICENSE("GPL");
Makefile如下:
1 KERN_DIR =/home/book/Documents/linux-2.6.22.6 2 PWD := $(shell pwd) 3 all: 4 make -C $(KERN_DIR) M=$(PWD) modules 5 6 clean: 7 make -C $(KERN_DIR) M=$(PWD) modules clean 8 rm -rf modules.order 9 10 obj-m += myled.o
上面的Makefile经过了一次更改,之前韦老师的Makefile如下:
关键在于使用韦老师的`pwd`这个方式,我在ubuntu 16.04上make会失败,查询网上资料,改成$(PWD)之后,终于make成功了。特别注意一点,在make驱动函数之前,需要先构建内核树,其实就是保证在make驱动函数之前,先make一下内核。还有一点需要注意,想要加载驱动,在第一次make内核之后,把此次生成的uImage下载进入flash,然后才可以看到驱动被加载。还有就是,在使用不更改uboot参数的网络文件系统,即通过手动mount的方式,这种情况下insmod驱动的.ko文件,会比较耗时,甚至容易出现失败或者长时间卡死状态,所以建议选用set uboot参数的方式,这样insmod的时候可以快速响应:
现在写个main函数测试这个驱动:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main(int argc, char **argv) { int fd; int val=1; fd=open("/dev/xxx",O_RDWR); if(fd<0) printf("can't open!\r\n"); write(fd,&val,4); return 0; }
在nfs共享目录下编译一下这个源文件:
生成可执行文件之后,在开发板上运行:
首先执行的时候,显示不能打开,因为我们还没有创建这样的设备,使用mknod创建设备节点之后,可以看到应用程序的open和write会触发我们驱动函数的open和write,证明我们的入门测试成功了。创建设备采用/dev/xxx是为了展示这个设备的名字,其实无关紧要,但是最好能有意义。
当然,这里只是我们第一个测试程序,存在不足,我们在驱动函数中是写死了主设备号为111,而且还需要手动创建节点,在之后的随笔中,将对其进行改进。
(现在我是使用的经过uboot更改了参数的nfs网络文件系统,这样的方式insmod更快)