(57)Linux驱动开发之三Linux字符设备驱动

 

一、通常状况下,对每一种设备驱动都会定义一个软件模块,这个工程模块包含.h和.c文件,前者定义该设备驱动的数据结构并声明外部函数,后者进行设备驱动的具体实现。

 

二、典型的无操做系统下的逻辑开发程序是:这种三层的裸机驱动模型是足够知足低耦合、高内聚的特色的。

 

 

 

三、当有操做系统存在时,设备驱动成为了链接硬件和内核的桥梁,这时候的设备驱动对外表现为操做系统的API,与直接裸机开发不一样,裸机开发时的设备驱动是应用工程师的API。若是设备驱动都按照操做系统给出的独立于设备的接口而设计,应用程序将可使用统一的系统调用接口来访问各类设备。

 

四、字符设备指须要经过串行顺序(一个字节一个字节访问)访问的设备;而块设备是能够任意顺序访问的设备,可是以块为单位进行操做。字符涉笔驱动和网络设备驱动都是使用文件系统的操做接口open(),close(),read(),write()函数来访问;可是内核与网络设备的通讯和内核与字符设备以及块设备的通讯方式就彻底不一样了。

 

 

五、编写linux设备驱动的技术基础:

 

(1)咱们在写驱动代码的时候,是直接在内核态下工做的,咱们使用的API是内核提供给咱们的,这套API(好比read()、printk()函数等)即设备驱动与内核的接口;咱们的内核统一一套这样的API或者说驱动框架,就是为了让咱们不一样设备的驱动能够相互独立出来。

 

(2)咱们学习设备驱动不仅是对一些内核与设备驱动接口的几个函数或者是几个数据结构了解就能够了,应该使用总体思惟、点面结合。

 

六、数字信号处理器(DSP)包括定点DSP和浮点DSP,其中浮点DSP是由硬件来实现的,优于定点DSP。

 

七、咱们能够得出的处理器分类:

 

 

 

 

八、存储器的分类:

 

 

 

九、I2C总线:该种总线用于链接微控制器及其外围设备,I2C总线支持多主控模式,任何可以进行发送和接收的设备都可以成为主设备,主控可以控制数据的传输和时钟频率,在任意一个时刻只能有一个主控。组成I2C总线的两个信号为数据线SDA和时钟线SCL。I2C设备上的串行数据线SDA接口电路是双向的,输出电路用于向总线发送数据,输入电路用于接收总线上的数据。

 

十、硬件时序分析:

 

时序分析的意思是让芯片之间的访问知足芯片手册中时序图信号有效的前后顺序、采样创建时间和保持时间的要求,在电路板工做不正常的时候准确的定位时序方面的问题。

 

创建时间:

 

保持时间:

 

十一、CPLD和FPGA

 

十二、示波器个逻辑分析仪在嵌入式方面的应用

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

 

一、POSIX标准:可移植的操做系统接口,该标准用于保证编制的应用程序能够在源代码一级上在多种操做系统上进行移植。

 

二、linux内核的组成部分:进程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、网络接口(NET)和进程间通讯(IPC)5个子系统组成。

 

(1)进程调度:

 

 

(2)内存管理:当CPU提供内存管理单元(MMU)时,内存管理系统会完成为每一个进程进行虚拟地址到物理内存的转化。0~3GB为进程空间,3~4GB为内核空间,内核空间对常规内存、I/O设备内存以及高端内存存在不一样的处理方式。

 

(3)虚拟文件系统:它隐藏了硬件的各类细节,为全部的设备提供了统一的接口,并且它独立于各个具体的文件系统,是对各类文件系统的一个抽象。

 

(4)网络接口:可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备进行通讯。

 

三、Linux系统只可以经过系统调用和硬件中断完成从用户空间到内核空间的控制转移。

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

 

                                                                                      linux内核的编译及加载

 

一、解压缩命令:tar -jxvf ~.tar.bz2

 

二、执行 make mrproper命令,确保没有出错的.o文件以及文件的互相依赖。

 

三、配置内核:make menuconfig命令

 

四、编译内核命令:make  bzImage    生成的镜像文件在:/usr

 

或者编译内核模块命令:make modules。

 

五、

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------

 

一、linux内核模块的可裁剪性,使内核体积不会特别大,动态加载。

 

二、lsmod命令能够得到系统中加载了的全部模块以及模块间的依赖关系,该命令实际上等同于  cat /proc/modules。

 

三、insmod:模块加载函数,insmod 某个目录下/xx.ko;还有一个模块加载命令 modprobe ,它在加载某个模块的时候会同时加载该模块所依赖的其余模块,使用 modinfo xxx.ko命令还能够得到模块的信息。

 

四、rmmod 用于卸载某个模块,它会调用模块卸载函数。

 

五、linux内核模块的程序结构:

 

(1)模块加载函数

 

static int  __init    init_function(void)     //__init  标识声明内核模块加载函数

 

{

 

/*初始化代码*/

 

}

 

module_init(init_function );

 

(2)模块卸载函数

 

static void  __exit   cleanup_function(void)     //__exit   标识声明内核模块卸载函数,无返回值

 

{

 

/*释放代码*/

 

}

 

module_exit(cleanup_function );

 

(3)模块声明与描述

 

 

 

(4)模块参数

 

module_param(参数名,参数类型,参数 读/写权限);在装载内核模块的时候,用户能够向内核模块传递参数,形式为: insmod 模块名 参数名=参数值。

 

eg:

 

 

 

 

(5)导出符号                     //没有实际意义

 

(6)模块的使用计数          //没有实际意义

 

(7)模块与GPL

 

为了使公司产品所使用的Linux操做系统支持模块,须要完成如下操做:

 

一、在内核编译时应该选择上"Enable loadable module support"

 

二、嵌入式产品在启动过程当中就应该加载模块,在这个启动过程当中加载企业本身的驱动模块最简单的方法就是修改启动过程当中的rc脚本,增长 insmod  /.../xx.ko这样的命令

 

 

 

                                                                               (8)模块的编译

 

内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(LoadableKernelModule,LKM),咱们简称为模块。 Linux内核之因此提供模块机制,是由于它自己是一个单内核(monolithickernel)。单内核的最大优势是效率高,由于全部的内容都集成在一块儿,但其缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。

 

1、什么是模块

 

模块是具备独立功能的程序,它能够被单独编译,但不能独立运行。它在运行时被连接到内核做为内核的一部分在内核空间运行,这与运行在用户空间的进程是不一样的。模块一般由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其余内核上层的功能。

 

应用程序与内核模块的比较

 

为了加深对内核模块的了解,表一给出应用程序与内核模块程序的比较。

 

表一应用程序与内核模块程序的比较

 

 

 

从表一咱们能够看出,内核模块程序不能调用libc库中的函数,它运行在内核空间,且只有超级用户能够对其运行。另外,模块程序必须经过module_init()和module-exit()函数来告诉内核“我来了”和“我走了”。

 

2、编写一个简单的模块

 

模块和内核都在内核空间运行,模块编程在必定意义上说就是内核编程。由于内核版本的每次变化,其中的某些函数名也会相应地发生变化,所以模块编程与内核版本密切相关。
1.程序举例

 

 
  1. #include  <module.h >
  2.  
  3. #include  <kernel.h >
  4.  
  5. #include  <init.h >
  6.  
  7. MODULE_LICENSE("GPL");  
  8.  
  9. staticint__initlkp_init(void)  
  10.  
  11. {  
  12.  
  13. printk(KERN_ALERT"HelloWorld!\n");  
  14.  
  15. return0;  
  16.  
  17. }  
  18.  
  19. staticvoid__exitlkp_cleanup(void)  
  20.  
  21. {  
  22.  
  23. printk(KERN_ALERT"ByeWorld!\n");  
  24.  
  25. }  
  26.  
  27. module_init(lkp_init);  
  28.  
  29. module_exit(lkp_cleanup);  
  30.  
  31. MODULE_AUTHOR("heyutao");  
  32.  
  33. MODULE_DESCRIPTION("hello"); 

 

说明
全部模块都要使用头文件module.h,此文件必须包含进来。
头文件kernel.h包含了经常使用的内核函数。
头文件init.h包含了宏_init和_exit,它们容许释放内核占用的内存。
lkp_init是模块的初始化函数,它必需包含诸如要编译的代码、初始化数据结构等内容。
使用了printk()函数,该函数是由内核定义的,功能与C库中的printf()相似,它把要打印的信息输出到终端或系统日志。
lkp_cleanup是模块的退出和清理函数。此处能够作全部终止该驱动程序时相关的清理工做。
module_init()和cleanup_exit()是模块编程中最基本也是必须的两个函数。
module_init()是驱动程序初始化的入口点。而cleanup_exit()注销由模块提供的全部功能。

 

2编写Makefile文件,与hello.c放在同一个目录里

 

 
  1. obj-m   :=hello.o
  2. KERNELBUILD:=/lib/modules/$(shelluname-r)/build
  3. default:
  4.          make -C $(KERNELBUILD) M=$(shellpwd) modules
  5. clean:
  6.          rm -rf *.o *.ko *.mod.c .*.cmd *.markers *.order *.symvers.tmp_versions 

 

(注意makefile里面要求的tab)
KERNELBUILD:=/lib/modules/$(shelluname-r)/build是编译内核模块须要的Makefile的路径,Ubuntu下是
/lib/modules/2.6.31-14-generic/build
make-C$(KERNELBUILD)M=$(shellpwd)modules编译内核模块。-C将工做目录转到KERNELBUILD,指定的是内核源代码的目录,调用该目录下的Makefile,并向这个Makefile传递参数。M的值是$(shellpwd)modules,咱们本身给他指定的目录。
3.编译模块
#sudo make(调用第一个命令default)
这时,在hello.c所在文件夹就会有hello.ko,这个就是咱们须要的内核模块
#sudo make clean
清理编译垃圾,hello.ko也会清理掉。

 

4.插入模块,让其工做。注意必须是root权限

 

#sudo insmod  ./hello.ko咱们用dmesg就能够看到产生的内核信息啦,Helloworld!

 

若是没有输出"hellofromhelloworld",由于若是你在字符终端而不是终端模拟器下运行的话,就会输出,由于在终端模拟器下时会把内核消息输出到日志文件/var/log/kern.log中。

 

#sudo rmmod./hello再用dmesg能够看到Byeworld!
备注:若是一个模块包含多个.c文件(eg:1.c,2.c),则应该使用以下方式编写Makefile,
obj-m :=modulename.o
module-objs := 1.o 2.o

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------

 

                                                                          linux文件系统与设备文件系统

 

一、linux文件系统VFS目录结构:

 

 

 

其中比较重要的有:

 

一、/bin目录,包含基本命令,如ls,cp,mkdir等,这个目录中的文件都是可执行的。

 

二、/dev目录,该目录是设备文件存储目录,应用程序经过对这些文件的读写和控制就能够访问实际的设备。

 

三、/etc目录,系统配置文件的所在地,一些服务器的配置文件

 

四、/lib目录,linux库文件存放目录。

 

五、/proc目录,操做系统运行时进程及内核信息(好比CPU、硬盘分区内存信息等)存放在这里,/proc目录为伪文件系统proc的挂载目录,proc并非真正的文件系统,它只是内核里一些数据结构在这一块的映射,它存在于内存之中。

 

六、/var目录,这个目录的内容常常变更,如/var/log目录被用来存放系统日志。

 

七、/sys,linux内核所支持的sysfs文件系统被映射在此目录,当内核检测到在系统中出现了新的设备后,内核会在sysfs文件系统中为该新设备生成一项新的记录。

 

 

 

 

 

 

 -----------------------------------------------------------------------------------------------------------------------------------------------------------  

 

                                                                                       字符设备驱动

 

 

 

1、字符设备基础知识
内核里有驱动,咱们操做这些驱动文件的方法是操纵内核给咱们提供的文件操做API,好比OPEN(),close()函数等。

 

一、设备驱动分类

 

      linux系统将设备分为3类:字符设备、块设备、网络设备。

 

 

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

 

块设备:是指能够从设备的任意位置读取必定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

 

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

 

二、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

 

 

 

在Linux内核中:

 

a -- 使用cdev结构体来描述字符设备;

 

b -- 经过其成员dev_t来定义设备号(分为主、次设备号)以肯定字符设备的惟一性;

 

c -- 经过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

 

在Linux字符设备驱动中:

 

a -- 模块加载函数经过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;

 

b -- 经过 cdev_init( ) 创建cdev与 file_operations之间的链接,经过 cdev_add( ) 向系统添加一个cdev以完成注册;

 

c -- 模块卸载函数经过cdev_del( )来注销cdev,经过 unregister_chrdev_region( )来释放设备号;

 

用户空间访问该设备的程序:

 

a -- 经过Linux系统调用,如open() 、                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;

 

三、字符设备驱动模型

 

 

 

2、cdev 结构体解析
      在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义以下:

 

[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. <include/linux/cdev.h>  
  2.   
  3. struct cdev {   
  4.     struct kobject kobj;                  //内嵌的内核对象.  
  5.     struct module *owner;                 //该字符设备所在的内核模块的对象指针.  
  6.     const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.  
  7.     struct list_head list;                //用来将已经向内核注册的全部字符设备造成链表.  
  8.     dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.  
  9.     unsigned int count;                   //隶属于同一主设备号的次设备号的个数.  
  10. };  
 
内核给出的操做struct cdev结构的接口主要有如下几个:

 

a -- void cdev_init(struct cdev *, const struct file_operations *);
其源代码如代码清单以下:

 

[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. void cdev_init(struct cdev *cdev, const struct file_operations *fops)  
  2. {  
  3.     memset(cdev, 0, sizeof *cdev);  
  4.     INIT_LIST_HEAD(&cdev->list);  
  5.     kobject_init(&cdev->kobj, &ktype_cdev_default);  
  6.     cdev->ops = fops;  
  7. }  
      该函数主要对struct cdev结构体作初始化,最重要的就是创建cdev 和 file_operations之间的链接:

 

(1) 将整个结构体清零;
(2) 初始化list成员使其指向自身;
(3) 初始化kobj成员;
(4) 初始化ops成员;
 
 b --struct cdev *cdev_alloc(void);
     该函数主要分配一个struct cdev结构,动态申请一个cdev内存,并作了cdev_init中所作的前面3步初始化工做(第四步初始化工做须要在调用cdev_alloc后,显式的作初始化即: .ops=xxx_ops).
其源代码清单以下:

 

[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. struct cdev *cdev_alloc(void)  
  2. {  
  3.     struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);  
  4.     if (p) {  
  5.         INIT_LIST_HEAD(&p->list);  
  6.         kobject_init(&p->kobj, &ktype_cdev_dynamic);  
  7.     }  
  8.     return p;  
  9. }  
     在上面的两个初始化的函数中,咱们没有看到关于owner成员、dev成员、count成员的初始化;其实,owner成员的存在体现了驱动程序与内核模块间的亲密关系,struct module是内核对于一个模块的抽象,该成员在字符设备中能够体现该设备隶属于哪一个模块,在驱动程序的编写中通常由用户显式的初始化 .owner = THIS_MODULE, 该成员能够防止设备的方法正在被使用时,设备所在模块被卸载。而dev成员和count成员则在cdev_add中才会赋上有效的值。
 
c -- int cdev_add(struct cdev *p, dev_t dev, unsigned count);
       该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p表明的字符设备已经可使用了。
固然这里还需提供两个参数:
(1)第一个设备号 dev,
(2)和该设备关联的设备编号的数量。
这两个参数直接赋值给struct cdev 的dev成员和count成员。
 
d -- void cdev_del(struct cdev *p);
     该函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p表明的字符设备已经不可使用了。
     从上述的接口讨论中,咱们发现对于struct cdev的初始化和注册的过程当中,咱们须要提供几个东西
(1) struct file_operations结构指针;
(2) dev设备号;
(3) count次设备号个数。
 
3、设备号相应操做
1 -- 主设备号和次设备号(两者一块儿为设备号):
      一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操做的是哪一个设备,用来区分同类型的设备。
  linux内核中,设备号用dev_t来描述,2.6.28中定义以下:
  typedef u_long dev_t;
  在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。
内核也为咱们提供了几个方便操做的宏实现dev_t:
1) -- 从设备号中提取major和minor
MAJOR(dev_t dev);                              
MINOR(dev_t dev);
2) -- 经过major和minor构建设备号
MKDEV(int major,int minor);
注:这只是构建设备号。并未注册,须要调用 register_chrdev_region 静态申请;

 

[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. //宏定义:  
  2. #define MINORBITS    20  
  3. #define MINORMASK    ((1U << MINORBITS) - 1)  
  4. #define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))  
  5. #define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))  
  6. #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))</span>  
 
二、分配设备号(两种方法):
a -- 静态申请:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
其源代码清单以下:
[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. int register_chrdev_region(dev_t from, unsigned count, const char *name)  
  2. {  
  3.     struct char_device_struct *cd;  
  4.     dev_t to = from + count;  
  5.     dev_t n, next;  
  6.   
  7.     for (n = from; n < to; n = next) {  
  8.         next = MKDEV(MAJOR(n)+1, 0);  
  9.         if (next > to)  
  10.             next = to;  
  11.         cd = __register_chrdev_region(MAJOR(n), MINOR(n),  
  12.                    next - n, name);  
  13.         if (IS_ERR(cd))  
  14.             goto fail;  
  15.     }  
  16.     return 0;  
  17. fail:  
  18.     to = n;  
  19.     for (n = from; n < to; n = next) {  
  20.         next = MKDEV(MAJOR(n)+1, 0);  
  21.         kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));  
  22.     }  
  23.     return PTR_ERR(cd);  
  24. }  
 
b -- 动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
其源代码清单以下:

 

[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,  
  2.             const char *name)  
  3. {  
  4.     struct char_device_struct *cd;  
  5.     cd = __register_chrdev_region(0, baseminor, count, name);  
  6.     if (IS_ERR(cd))  
  7.         return PTR_ERR(cd);  
  8.     *dev = MKDEV(cd->major, cd->baseminor);  
  9.     return 0;  
  10. }  
能够看到两者都是调用了__register_chrdev_region 函数,其源代码以下:
[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. static struct char_device_struct *  
  2. __register_chrdev_region(unsigned int major, unsigned int baseminor,  
  3.                int minorct, const char *name)  
  4. {  
  5.     struct char_device_struct *cd, **cp;  
  6.     int ret = 0;  
  7.     int i;  
  8.   
  9.     cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);  
  10.     if (cd == NULL)  
  11.         return ERR_PTR(-ENOMEM);  
  12.   
  13.     mutex_lock(&chrdevs_lock);  
  14.   
  15.     /* temporary */  
  16.     if (major == 0) {  
  17.         for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {  
  18.             if (chrdevs[i] == NULL)  
  19.                 break;  
  20.         }  
  21.   
  22.         if (i == 0) {  
  23.             ret = -EBUSY;  
  24.             goto out;  
  25.         }  
  26.         major = i;  
  27.         ret = major;  
  28.     }  
  29.   
  30.     cd->major = major;  
  31.     cd->baseminor = baseminor;  
  32.     cd->minorct = minorct;  
  33.     strlcpy(cd->name, name, sizeof(cd->name));  
  34.   
  35.     i = major_to_index(major);  
  36.   
  37.     for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)  
  38.         if ((*cp)->major > major ||  
  39.             ((*cp)->major == major &&  
  40.              (((*cp)->baseminor >= baseminor) ||  
  41.               ((*cp)->baseminor + (*cp)->minorct > baseminor))))  
  42.             break;  
  43.   
  44.     /* Check for overlapping minor ranges.  */  
  45.     if (*cp && (*cp)->major == major) {  
  46.         int old_min = (*cp)->baseminor;  
  47.         int old_max = (*cp)->baseminor + (*cp)->minorct - 1;  
  48.         int new_min = baseminor;  
  49.         int new_max = baseminor + minorct - 1;  
  50.   
  51.         /* New driver overlaps from the left.  */  
  52.         if (new_max >= old_min && new_max <= old_max) {  
  53.             ret = -EBUSY;  
  54.             goto out;  
  55.         }  
  56.   
  57.         /* New driver overlaps from the right.  */  
  58.         if (new_min <= old_max && new_min >= old_min) {  
  59.             ret = -EBUSY;  
  60.             goto out;  
  61.         }  
  62.     }  
  63.   
  64.     cd->next = *cp;  
  65.     *cp = cd;  
  66.     mutex_unlock(&chrdevs_lock);  
  67.     return cd;  
  68. out:  
  69.     mutex_unlock(&chrdevs_lock);  
  70.     kfree(cd);  
  71.     return ERR_PTR(ret);  
  72. }  
 经过这个函数能够看出 register_chrdev_region和 alloc_chrdev_region 的区别,register_chrdev_region静态申请的方式是直接将Major 注册进入,而 alloc_chrdev_region动态分配的方式是从Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;

 

两者应用能够简单总结以下:
                                     register_chrdev_region                                                alloc_chrdev_region 

 

    devno = MKDEV(major,minor);
    ret = register_chrdev_region(devno, 1, "hello"); 
    cdev_init(&cdev,&hello_ops);
    ret = cdev_add(&cdev,devno,1);
    alloc_chrdev_region(&devno, minor, 1, "hello");
    major = MAJOR(devno);
    cdev_init(&cdev,&hello_ops);
    ret = cdev_add(&cdev,devno,1)
register_chrdev(major,"hello",&hello

 

     能够看到,除了前面两个函数,还加了一个register_chrdev 函数,能够发现这个函数的应用很是简单,只要一句就能够搞定前面函数所作之事;
下面分析一下register_chrdev 函数,其源代码定义以下:

 

[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. static inline int register_chrdev(unsigned int major, const char *name,  
  2.                   const struct file_operations *fops)  
  3. {  
  4.     return __register_chrdev(major, 0, 256, name, fops);  
  5. }  
调用了 __register_chrdev(major, 0, 256, name, fops) 函数:
[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. int __register_chrdev(unsigned int major, unsigned int baseminor,  
  2.               unsigned int count, const char *name,  
  3.               const struct file_operations *fops)  
  4. {  
  5.     struct char_device_struct *cd;  
  6.     struct cdev *cdev;  
  7.     int err = -ENOMEM;  
  8.   
  9.     cd = __register_chrdev_region(major, baseminor, count, name);  
  10.     if (IS_ERR(cd))  
  11.         return PTR_ERR(cd);  
  12.   
  13.     cdev = cdev_alloc();  
  14.     if (!cdev)  
  15.         goto out2;  
  16.   
  17.     cdev->owner = fops->owner;  
  18.     cdev->ops = fops;  
  19.     kobject_set_name(&cdev->kobj, "%s", name);  
  20.   
  21.     err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);  
  22.     if (err)  
  23.         goto out;  
  24.   
  25.     cd->cdev = cdev;  
  26.   
  27.     return major ? 0 : cd->major;  
  28. out:  
  29.     kobject_put(&cdev->kobj);  
  30. out2:  
  31.     kfree(__unregister_chrdev_region(cd->major, baseminor, count));  
  32.     return err;  
  33. }  
能够看到这个函数不仅帮咱们注册了设备号,还帮咱们作了cdev 的初始化以及cdev 的注册;
 

 

三、注销设备号:
void unregister_chrdev_region(dev_t from, unsigned count);
 
四、建立设备文件:
     利用cat /proc/devices查看申请到的设备名,设备号。
1)使用mknod手工建立:mknod filename type major minor
2)自动建立设备节点:
    利用udev(mdev)来实现设备文件的自动建立,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备建立一个class,再为每一个设备调用device_create建立对应的设备。
 
下面看一个实例,练习一下上面的操做:
hello.c
  1. #include <linux/module.h>  
  2. #include <linux/fs.h>  
  3. #include <linux/cdev.h>  
  4. static int major = 250;  
  5. static int minor = 0;  
  6. static dev_t devno;  
  7. static struct cdev cdev;  
  8. static int hello_open (struct inode *inode, struct file *filep)  
  9. {  
  10.     printk("hello_open \n");  
  11.     return 0;  
  12. }  
  13. static struct file_operations hello_ops=  
  14. {  
  15.     .open = hello_open,           
  16. };  
  17.   
  18. static int hello_init(void)  
  19. {  
  20.     int ret;      
  21.     printk("hello_init");  
  22.     devno = MKDEV(major,minor);  
  23.     ret = register_chrdev_region(devno, 1, "hello");  
  24.     if(ret < 0)  
  25.     {  
  26.         printk("register_chrdev_region fail \n");  
  27.         return ret;  
  28.     }  
  29.     cdev_init(&cdev,&hello_ops);  
  30.     ret = cdev_add(&cdev,devno,1);  
  31.     if(ret < 0)  
  32.     {  
  33.         printk("cdev_add fail \n");  
  34.         return ret;  
  35.     }     
  36.     return 0;  
  37. }  
  38. static void hello_exit(void)  
  39. {  
  40.     cdev_del(&cdev);  
  41.     unregister_chrdev_region(devno,1);  
  42.     printk("hello_exit \n");  
  43. }  
  44. MODULE_LICENSE("GPL");                 //GPL许可声明
  45. module_init(hello_init);               //初始化函数声明
  46. module_exit(hello_exit);               //注销函数声明
测试程序 test.c
[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. #include <sys/types.h>  
  2. #include <sys/stat.h>  
  3. #include <fcntl.h>  
  4. #include <stdio.h>  
  5.   
  6. main()  
  7. {  
  8.     int fd;  
  9.   
  10.     fd = open("/dev/hello",O_RDWR);  
  11.     if(fd<0)  
  12.     {  
  13.         perror("open fail \n");  
  14.         return ;  
  15.     }  
  16.   
  17.     close(fd);  
  18. }  
makefile:
[cpp] view plain copy 在CODE上查看代码片派生到个人代码片
  1. ifneq  ($(KERNELRELEASE),)  
  2. obj-m:=hello.o  
  3. $(info "2nd")  
  4. else  
  5. KDIR := /lib/modules/$(shell uname -r)/build  
  6. PWD:=$(shell pwd)  
  7. all:  
  8.     $(info "1st")  
  9.     make -C $(KDIR) M=$(PWD) modules  
  10. clean:  
  11.     rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order  
  12. endif  

 

编译成功后,使用 insmod 命令加载:
而后用cat /proc/devices 查看,会发现设备号已经申请成功;
 

 

 

 

----------------------------------------------------------------------------------------------------------------------------------------------------------

 

linux字符设备驱动结构:

 

一、cdev结构体:

 

cdev结构体用来描述字符设备,这个结构体中的一个重要的成员,这个结构体中的一个重要成员:file_operations 定义了字符设备驱动提供给虚拟文件系统的接口函数。

 

struct cdev {

 

struct kobject kobj;          // 每一个 cdev都是一个 kobject

 

struct module *owner;       //指 向实现驱动的模块

 

const struct file_operations *ops;   // 操纵这个字符设备文件的方法

 

struct list_head list;       // 与 cdev对应的字符设备文件的inode->i_devices的链表头

 

dev_t dev;                  // 起始设备编号

 

unsigned int count;       // 设备范围号大小

 

};

 

一个 cdev通常它有两种定义初始化方式:静态的和动态的。

 

静态内存定义初始化:

 

struct cdev my_cdev;

 

cdev_init(&my_cdev, &fops);

 

my_cdev.owner = THIS_MODULE;

 

动态内存定义初始化:

 

struct cdev *my_cdev = cdev_alloc();

 

my_cdev->ops = &fops;

 

my_cdev->owner = THIS_MODULE;

 

两种使用方式的功能是同样的,只是使用的内存区不同,通常视实际的数据结构需求而定。

 

linux内核向系统提供的操做函数API:

 

(1)void cdev_init(struct cdev *, struct  file_operations  *);//这个函数用于初始化cdev成员,并创建cdev和file_operations之间的链接。

 

(2)struct cdev *cdev_alloc(void);                                      //动态申请一个cdev内存,返回一个结构体指针类型

 

(3)void cdev_put(struct cdev *p);

 

(4)int cdev_add(struct cdev *, devt_t,unsigned);             //向系统添加一个cdev,完成字符设备的注册,发生在字符设备驱动模块加载函数中

 

(5)void cdev_del (struct cdev *);                                      //向系统删除一个cdev,完成字符设备的注销,发生在字符设备驱动模块卸载函数中

 

 

 

二、分配和释放设备号:

 

 

 

 创建一个字符设备以前,驱动程序首先要作的事情就是得到设备编号。其这主要函数在<linux/fs.h>中声明:

 

 

 

//指定设备编号在调用cdev_add()函数以前
int  register_chrdev_region(dev_t from, unsigned int count,const char  *name); 
//动态生成设备编号,向系统动态申请未被占用的设备号的状况,在调用cdev_add()函数以前
int alloc_chrdev_region(dev_t  *dev, unsigned int baseminor,unsigned int count, const char *name); 
//释放设备编号,在调用cdev_del()函数以后
void unregister_chrdev_region(dev_t from, unsigned int count);

 

 

 

分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。

 

 

 

 

 

三、file_operations结构体

 

 

 

结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各类操做的函数的指针。该结构体的每一个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。

 

举个例子,每一个字符设备须要定义一个用来读取设备数据的函数。结构体 file_operations中存储着内核模块中执行这项操做的函数的地址。一下是该结构体 在内核2.6.5中看起来的样子:

 

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(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);

 

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

 

  ssize_t(*aio_write) (struct kiocb *, const char __user *, 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 (*aio_fsync) (struct kiocb *, 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(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);

 

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

 

};

 

驱动内核模块是不须要实现每一个函数的。像视频卡的驱动就不须要从目录的结构 中读取数据。那么,相对应的file_operations重的项就为 NULL。

 

gcc还有一个方便使用这种结构体的扩展。你会在较现代的驱动内核模块中见到。新的使用这种结构体的方式以下:

 

struct file_operations fops = {

 

 read: device_read,

 

 write: device_write,

 

 open: device_open,

 

 release: device_release

 

};

 

一样也有C99语法的使用该结构体的方法,而且它比GNU扩展更受推荐。我使用的版本为 2.95为了方便那些想移植你的代码的人,你最好使用这种语法。它将提升代码的兼容性:

 

struct file_operations fops = {

 

 .read = device_read,

 

 .write = device_write,

 

 .open = device_open,

 

 .release = device_release

 

};

 

这种语法很清晰,你也必须清楚的意识到没有显示声明的结构体成员都被gcc初始化为NULL。

 

指向结构体struct file_operations的指针一般命名为fops。

 

 

 

关于file结构体

 

每个设备文件都表明着内核中的一个file结构体。该结构体在头文件linux/fs.h定义。注意,file结构体是内核空间的结构体, 这意味着它不会在用户程序的代码中出现。它绝对不是在glibc中定义的FILE。 FILE本身也从不在内核空间的函数中出现。它的名字确实挺让人迷惑的。它表明着一个抽象的打开的文件,但不是那种在磁盘上用结构体inode表示的文件。

 

指向结构体struct file的指针一般命名为filp。你一样能够看到struct file file的表达方式,但不要被它诱惑。

 

去看看结构体file的定义。大部分的函数入口,像结构体struct dentry没有被设备驱动模块使用,你大可忽略它们。这是由于设备驱动模块并不本身直接填充结构体file:它们只是使用在别处创建的结构体file中的数据。

 

 

 

注册一个设备

 

如同先前讨论的,字符设备一般经过在路径/dev下的设备文件进行访问。主设备号告诉你哪些驱动模块是用来操纵哪些硬件设备的。从设备号是驱动模块本身使用来区别它操纵的不一样设备,当此驱动模块操纵不仅一个设备时。

 

将内核驱动模块加载入内核意味着要向内核注册本身。这个工做是和驱动模块得到主设备号时初始化一同进行的。你可使用头文件linux/fs.h中的函数register_chrdev来实现。

 

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

 

其中unsigned int major是你申请的主设备号,const char *name是将要在文件/proc/devices中显示的名称,struct file_operations *fops是指向你的驱动模块的file_operations表的指针。负的返回值意味着注册失败。注意注册并不须要提供从设备号。内核自己并不在乎从设备号。

 

如今的问题是你如何申请到一个没有被使用的主设备号?最简单的方法是查看文件 Documentation/devices.txt从中挑选一个没有被使用的。这不是一劳永逸的方法由于你没法得知该主设备号在未来会被占用。最终的方法是让内核为你动态分配一个。

 

若是你向函数register_chrdev传递为0的主设备号,那么返回的就是动态分配的主设备号。反作用就是既然你没法得知主设备号,你就没法预先创建一个设备文件。有多种解决方法:第一种方法是新注册的驱动模块会输出本身新分配到的主设备号,因此咱们能够手工创建须要的设备文件。第二种是利用文件/proc/devices新注册的驱动模块的入口,要么手工创建设备文件,要么编一个脚本去自动读取该文件而且生成设备文件。第三种是在咱们的模块中,当注册成功时,使用mknod系统调用创建设备文件而且在驱动模块调用函数cleanup_module前,调用rm删除该设备文件。

 

 

 

 

 

---------------------------------------------------------------------------------------------------------------------------------------------

 

                                                                               linux字符设备驱动的组成

 

一、字符设备驱动模块的加载与卸载函数:

 

在 字符设备驱动模块的加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中应该实现设备号的释放和cdev的注销。

 

 

 

 

 

 

 

 

 

 

 

---------------------------------------------------------------------------------------------------------------------------------------------

 

                                                                             linux设备驱动中的并发控制

 

 

 

一、访问共享资源的代码区域称为临界区,临界区须要以某种互斥机制加以保护。eg:中断屏蔽、原子操做、自旋锁和信号量是linux设备驱动中能够采用预防并发的有效方法。

 

(1)中断屏蔽:该方法使中断与进程之间的并发再也不发生。

 

local_irq_disable()    //屏蔽中断

 

。。。

 

critical section()       //临界区

 

。。。

 

local_irq_enable()     //开中断

 

(2)原子操做:指的是在执行过程当中不会被别的代码路径所中断的操做。

 

整型原子操做:

 

 

 

位原子操做:

 

(3)自旋锁:指对临界资源进行互斥访问的一种方式。
相关文章
相关标签/搜索