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

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

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

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

4、字符设备指需要通过串行顺序(一个字节一个字节访问)访问的设备;而块设备是可以任意顺序访问的设备,但是以块为单位进行操作。字符涉笔驱动和网络设备驱动都是使用文件系统的操作接口open(),close(),read(),write()函数来访问;但是内核与网络设备的通信和内核与字符设备以及块设备的通信方式就完全不同了。

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

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

(2)我们学习设备驱动不只是对一些内核与设备驱动接口的几个函数或者是几个数据结构了解就可以了,应该使用整体思维、点面结合。

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

7、我们可以得出的处理器分类:

8、存储器的分类:

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

10、硬件时序分析:

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

建立时间:

保持时间:

11、CPLD和FPGA

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

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

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

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

(1)进程调度:

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

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

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

3、Linux系统只能够通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。

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

linux内核的编译及加载

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

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

3、配置内核:make menuconfig命令

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

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

5、

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

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

2、lsmod命令可以获得系统中加载了的所有模块以及模块间的依赖关系,该命令实际上等同于  cat /proc/modules。

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

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

5、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操作系统支持模块,需要完成以下操作:

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

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

(8)模块的编译

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

一、什么是模块

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

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

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

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

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

二、编写一个简单的模块

模块和内核都在内核空间运行,模块编程在一定意义上说就是内核编程。因为内核版本的每次变化,其中的某些函数名也会相应地发生变化,因此模块编程与内核版本密切相关。

1.程序举例

  1. #include  <module.h >
  2. #include  <kernel.h >
  3. #include  <init.h >
  4. MODULE_LICENSE("GPL");
  5. staticint__initlkp_init(void)
  6. {
  7. printk(KERN_ALERT"HelloWorld!\n");
  8. return0;
  9. }
  10. staticvoid__exitlkp_cleanup(void)
  11. {
  12. printk(KERN_ALERT"ByeWorld!\n");
  13. }
  14. module_init(lkp_init);
  15. module_exit(lkp_cleanup);
  16. MODULE_AUTHOR("heyutao");
  17. 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文件系统与设备文件系统

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

其中比较重要的有:

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

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

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

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

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

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

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

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

字符设备驱动

一、字符设备基础知识

内核里有驱动,我们操作这些驱动文件的方法是操纵内核给我们提供的文件操作API,比如OPEN(),close()函数等。

1、设备驱动分类

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

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

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

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

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

在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的接口函数;

3、字符设备驱动模型

二、cdev 结构体解析

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

[cpp] view plain copy 

  1. <include/linux/cdev.h>
  2. struct cdev {
  3. struct kobject kobj;                  //内嵌的内核对象.
  4. struct module *owner;                 //该字符设备所在的内核模块的对象指针.
  5. const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
  6. struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.
  7. dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.
  8. unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
  9. };

内核给出的操作struct cdev结构的接口主要有以下几个:

a -- void cdev_init(struct cdev *, const struct file_operations *);

其源代码如代码清单如下:

[cpp] view plain copy 

  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 

  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次设备号个数。

三、设备号相应操作

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 

  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>

2、分配设备号(两种方法):

a -- 静态申请:

int register_chrdev_region(dev_t from, unsigned count, const char *name);

其源代码清单如下:

[cpp] view plain copy 

  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. for (n = from; n < to; n = next) {
  7. next = MKDEV(MAJOR(n)+1, 0);
  8. if (next > to)
  9. next = to;
  10. cd = __register_chrdev_region(MAJOR(n), MINOR(n),
  11. next - n, name);
  12. if (IS_ERR(cd))
  13. goto fail;
  14. }
  15. return 0;
  16. fail:
  17. to = n;
  18. for (n = from; n < to; n = next) {
  19. next = MKDEV(MAJOR(n)+1, 0);
  20. kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
  21. }
  22. return PTR_ERR(cd);
  23. }

b -- 动态分配:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

其源代码清单如下:

[cpp] view plain copy 

  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 

  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. cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
  9. if (cd == NULL)
  10. return ERR_PTR(-ENOMEM);
  11. mutex_lock(&chrdevs_lock);
  12. /* temporary */
  13. if (major == 0) {
  14. for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
  15. if (chrdevs[i] == NULL)
  16. break;
  17. }
  18. if (i == 0) {
  19. ret = -EBUSY;
  20. goto out;
  21. }
  22. major = i;
  23. ret = major;
  24. }
  25. cd->major = major;
  26. cd->baseminor = baseminor;
  27. cd->minorct = minorct;
  28. strlcpy(cd->name, name, sizeof(cd->name));
  29. i = major_to_index(major);
  30. for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
  31. if ((*cp)->major > major ||
  32. ((*cp)->major == major &&
  33. (((*cp)->baseminor >= baseminor) ||
  34. ((*cp)->baseminor + (*cp)->minorct > baseminor))))
  35. break;
  36. /* Check for overlapping minor ranges.  */
  37. if (*cp && (*cp)->major == major) {
  38. int old_min = (*cp)->baseminor;
  39. int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
  40. int new_min = baseminor;
  41. int new_max = baseminor + minorct - 1;
  42. /* New driver overlaps from the left.  */
  43. if (new_max >= old_min && new_max <= old_max) {
  44. ret = -EBUSY;
  45. goto out;
  46. }
  47. /* New driver overlaps from the right.  */
  48. if (new_min <= old_max && new_min >= old_min) {
  49. ret = -EBUSY;
  50. goto out;
  51. }
  52. }
  53. cd->next = *cp;
  54. *cp = cd;
  55. mutex_unlock(&chrdevs_lock);
  56. return cd;
  57. out:
  58. mutex_unlock(&chrdevs_lock);
  59. kfree(cd);
  60. return ERR_PTR(ret);
  61. }

通过这个函数可以看出 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 

  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 

  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. cd = __register_chrdev_region(major, baseminor, count, name);
  9. if (IS_ERR(cd))
  10. return PTR_ERR(cd);
  11. cdev = cdev_alloc();
  12. if (!cdev)
  13. goto out2;
  14. cdev->owner = fops->owner;
  15. cdev->ops = fops;
  16. kobject_set_name(&cdev->kobj, "%s", name);
  17. err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
  18. if (err)
  19. goto out;
  20. cd->cdev = cdev;
  21. return major ? 0 : cd->major;
  22. out:
  23. kobject_put(&cdev->kobj);
  24. out2:
  25. kfree(__unregister_chrdev_region(cd->major, baseminor, count));
  26. return err;
  27. }

可以看到这个函数不只帮我们注册了设备号,还帮我们做了cdev 的初始化以及cdev 的注册;

3、注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count);

4、创建设备文件:

利用cat /proc/devices查看申请到的设备名,设备号。

1)使用mknod手工创建:mknod filename type major minor

2)自动创建设备节点:

利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

详细解析见:Linux 字符设备驱动开发 (二)—— 自动创建设备节点

下面看一个实例,练习一下上面的操作:

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. static int hello_init(void)
  18. {
  19. int ret;
  20. printk("hello_init");
  21. devno = MKDEV(major,minor);
  22. ret = register_chrdev_region(devno, 1, "hello");
  23. if(ret < 0)
  24. {
  25. printk("register_chrdev_region fail \n");
  26. return ret;
  27. }
  28. cdev_init(&cdev,&hello_ops);
  29. ret = cdev_add(&cdev,devno,1);
  30. if(ret < 0)
  31. {
  32. printk("cdev_add fail \n");
  33. return ret;
  34. }
  35. return 0;
  36. }
  37. static void hello_exit(void)
  38. {
  39. cdev_del(&cdev);
  40. unregister_chrdev_region(devno,1);
  41. printk("hello_exit \n");
  42. }
  43. MODULE_LICENSE("GPL");                 //GPL许可声明
  44. module_init(hello_init);               //初始化函数声明
  45. module_exit(hello_exit);               //注销函数声明

测试程序 test.c

[cpp] view plain copy 

  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <stdio.h>
  5. main()
  6. {
  7. int fd;
  8. fd = open("/dev/hello",O_RDWR);
  9. if(fd<0)
  10. {
  11. perror("open fail \n");
  12. return ;
  13. }
  14. close(fd);
  15. }

makefile:

[cpp] view plain copy 

  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字符设备驱动结构:

1、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,完成字符设备的注销,发生在字符设备驱动模块卸载函数中

2、分配和释放设备号:

建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在<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);


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

3、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字符设备驱动的组成

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

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

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

linux设备驱动中的并发控制

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

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

local_irq_disable()    //屏蔽中断

。。。

critical section()       //临界区

。。。

local_irq_enable()     //开中断

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

整型原子操作:

位原子操作:

(3)自旋锁:指对临界资源进行互斥访问的一种方式。

时间: 2024-10-02 16:30:34

(57)Linux驱动开发之三Linux字符设备驱动的相关文章

arm-linux字符设备驱动开发之---简单字符设备驱动

一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 1.字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流的设备,常见的字符设备有鼠标.键盘.串口.控制台和LED设备等.2.块设备:是指可以从设备的任意位置读取一定长度数据的设备.块设备包括硬盘.磁盘.U盘和SD卡等. 每一个字符设备或块设备都在/dev目录下对应一个设备文件.linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和

Linux嵌入式驱动学习之路⑩字符设备驱动-my_led

首先贴上代码: 字符设备驱动代码: /** *file name: led.c */#include <linux/sched.h> #include <linux/signal.h> #include <linux/spinlock.h> #include <linux/errno.h> #include <linux/random.h> #include <linux/poll.h> #include <linux/init

Linux嵌入式学习-烟雾传感器驱动-字符设备驱动-按键驱动

MQ-2烟雾气敏传感器模块在X210v3开发板上的驱动. 现在需要一个MQ-2烟雾气敏传感器模块的驱动.其检测烟雾超过一定的标准后,会返回一个不同的电平,和按键驱动差不多. 但是在编写驱动的时候,需要用GPH2_3号引脚.但是在内核中先ioremap地址然后配置,发现无法控制gpio,也无法进入中断. 后来发现,如果需要使用gpio,需要先申请,然后才能使用. 具体程序如下: #include <linux/module.h> #include <linux/init.h> #in

20150518 字符设备驱动

20150518 字符设备驱动 2015-05-18 Lover雪儿 经过这两个月的学习,相信对设备驱动的编写已经有一个大概的了解了,温故而知新,此处我们再一次的系统性的复习一下字符设备驱动,然后,我们来尝试着自己从零实战写一个AD采集的字符设备驱动. 以前学习使用的是老方法来注册字符设备驱动,此处我们使用字符设备的新方法来学习. 本文参考:华清远见的Linux 设备驱动开发详解-字符设备驱动,具体还请看作者原书 一.cdev结构体 1 struct cdev{ 2 struct kobject

从Linux内核LED驱动来理解字符设备驱动开发流程

目录 博客说明 开发环境 1. Linux字符设备驱动的组成 1.1 字符设备驱动模块加载与卸载函数 1.2 字符设备驱动的file_operations 结构体中的成员函数 2. 字符设备驱动--设备号注册卸载 2.1 设备号注册 2.2 设备号注销 3. 字符设备驱动--文件操作 参考资料 示例代码 @(从Linux内核LED驱动来理解字符设备驱动开发流程) 博客说明 撰写日期 2018.12.08 完稿日期 2019.10.06 最近维护 暂无 本文作者 multimicro 联系方式 [

Linux内核分析(五)----字符设备驱动实现

原文:Linux内核分析(五)----字符设备驱动实现 Linux内核分析(五) 昨天我们对linux内核的子系统进行简单的认识,今天我们正式进入驱动的开发,我们今后的学习为了避免大家没有硬件的缺陷,我们都会以虚拟的设备为例进行学习,所以大家不必害怕没有硬件的问题. 今天我们会分析到以下内容: 1.      字符设备驱动基础 2.      简单字符设备驱动实现 3.      驱动测试 l  字符设备基础 1.       字符设备描述结构 在linux2.6内核中,使用cdev结构体描述一

字符设备驱动、平台设备驱动、设备驱动模型、sysfs的关系

Linux驱动开发的童鞋们来膜拜吧:-)  学习Linux设备驱动开发的过程中自然会遇到字符设备驱动.平台设备驱动.设备驱动模型和sysfs等相关概念和技术.对于初学者来说会非常困惑,甚至对Linux有一定基础的工程师而言,能够较好理解这些相关技术也相对不错了.要深刻理解其中的原理需要非常熟悉设备驱动相关的框架和模型代码.网络上有关这些技术的文章不少,但多是对其中的某一点进行阐述,很难找到对这些技术进行比较和关联的分析.对于开发者而言,能够熟悉某一点并分享出来已很难得,但对于专注传授技术和经验给

[kernel]字符设备驱动、平台设备驱动、设备驱动模型、sysfs几者之间的比较和关联

转自:http://www.2cto.com/kf/201510/444943.html Linux驱动开发经验总结,绝对干货! 学习Linux设备驱动开发的过程中自然会遇到字符设备驱动.平台设备驱动.设备驱动模型和sysfs等相关概念和技术.对于初学者来说会非常困惑,甚至对Linux有一定基础的工程师而言,能够较好理解这些相关技术也相对不错了.要深刻理解其中的原理需要非常熟悉设备驱动相关的框架和模型代码.网络上有关这些技术的文章不少,但多是对其中的某一点进行阐述,很难找到对这些技术进行比较和关

第一个字符设备驱动

转载请注明出处:http://blog.csdn.net/ruoyunliufeng/article/details/45054183 linux驱动分为字符设备.块设备驱动.网络驱动三种,其中以字符驱动最为简单.说起要写驱动自然想到从字符设备驱动写起.看了开发板官方的驱动代码,对新手来说简直是噩梦.新手来说要看懂,实在不容易.其中包含了很多知识和设计思想.所以我想还是尽可能从易到难来写这个系列,相信我,我会努力把我知道的都给大家讲清楚. 一.驱动代码 /* * This program is