(57)Linux驱动开发之三Linux字符设备驱动
Posted 工业物联网集成了微电子计算技术、通信技术、云平台、大数据技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(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.程序举例
- #include <module.h >
- #include <kernel.h >
- #include <init.h >
- MODULE_LICENSE("GPL");
- staticint__initlkp_init(void)
- {
- printk(KERN_ALERT"HelloWorld!\\n");
- return0;
- }
- staticvoid__exitlkp_cleanup(void)
- {
- printk(KERN_ALERT"ByeWorld!\\n");
- }
- module_init(lkp_init);
- module_exit(lkp_cleanup);
- MODULE_AUTHOR("heyutao");
- 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放在同一个目录里
- obj-m :=hello.o
- KERNELBUILD:=/lib/modules/$(shelluname-r)/build
- default:
- make -C $(KERNELBUILD) M=$(shellpwd) modules
- clean:
- 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
- <include/linux/cdev.h>
- struct cdev {
- struct kobject kobj; //内嵌的内核对象.
- struct module *owner; //该字符设备所在的内核模块的对象指针.
- const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
- struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
- dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
- unsigned int count; //隶属于同一主设备号的次设备号的个数.
- };
内核给出的操作struct cdev结构的接口主要有以下几个:
a -- void cdev_init(struct cdev *, const struct file_operations *);
其源代码如代码清单如下:
[cpp] view plain copy
- void cdev_init(struct cdev *cdev, const struct file_operations *fops)
- {
- memset(cdev, 0, sizeof *cdev);
- INIT_LIST_HEAD(&cdev->list);
- kobject_init(&cdev->kobj, &ktype_cdev_default);
- cdev->ops = fops;
- }
该函数主要对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
- struct cdev *cdev_alloc(void)
- {
- struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
- if (p) {
- INIT_LIST_HEAD(&p->list);
- kobject_init(&p->kobj, &ktype_cdev_dynamic);
- }
- return p;
- }
在上面的两个初始化的函数中,我们没有看到关于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
- //宏定义:
- #define MINORBITS 20
- #define MINORMASK ((1U << MINORBITS) - 1)
- #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
- #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
- #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
- int register_chrdev_region(dev_t from, unsigned count, const char *name)
- {
- struct char_device_struct *cd;
- dev_t to = from + count;
- dev_t n, next;
- for (n = from; n < to; n = next) {
- next = MKDEV(MAJOR(n)+1, 0);
- if (next > to)
- next = to;
- cd = __register_chrdev_region(MAJOR(n), MINOR(n),
- next - n, name);
- if (IS_ERR(cd))
- goto fail;
- }
- return 0;
- fail:
- to = n;
- for (n = from; n < to; n = next) {
- next = MKDEV(MAJOR(n)+1, 0);
- kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
- }
- return PTR_ERR(cd);
- }
b -- 动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
其源代码清单如下:
[cpp] view plain copy
- int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
- const char *name)
- {
- struct char_device_struct *cd;
- cd = __register_chrdev_region(0, baseminor, count, name);
- if (IS_ERR(cd))
- return PTR_ERR(cd);
- *dev = MKDEV(cd->major, cd->baseminor);
- return 0;
- }
可以看到二者都是调用了__register_chrdev_region 函数,其源代码如下:
[cpp] view plain copy
- static struct char_device_struct *
- __register_chrdev_region(unsigned int major, unsigned int baseminor,
- int minorct, const char *name)
- {
- struct char_device_struct *cd, **cp;
- int ret = 0;
- int i;
- cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
- if (cd == NULL)
- return ERR_PTR(-ENOMEM);
- mutex_lock(&chrdevs_lock);
- /* temporary */
- if (major == 0) {
- for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
- if (chrdevs[i] == NULL)
- break;
- }
- if (i == 0) {
- ret = -EBUSY;
- goto out;
- }
- major = i;
- ret = major;
- }
- cd->major = major;
- cd->baseminor = baseminor;
- cd->minorct = minorct;
- strlcpy(cd->name, name, sizeof(cd->name));
- i = major_to_index(major);
- for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
- if ((*cp)->major > major ||
- ((*cp)->major == major &&
- (((*cp)->baseminor >= baseminor) ||
- ((*cp)->baseminor + (*cp)->minorct > baseminor))))
- break;
- /* Check for overlapping minor ranges. */
- if (*cp && (*cp)->major == major) {
- int old_min = (*cp)->baseminor;
- int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
- int new_min = baseminor;
- int new_max = baseminor + minorct - 1;
- /* New driver overlaps from the left. */
- if (new_max >= old_min && new_max <= old_max) {
- ret = -EBUSY;
- goto out;
- }
- /* New driver overlaps from the right. */
- if (new_min <= old_max && new_min >= old_min) {
- ret = -EBUSY;
- goto out;
- }
- }
- cd->next = *cp;
- *cp = cd;
- mutex_unlock(&chrdevs_lock);
- return cd;
- out:
- mutex_unlock(&chrdevs_lock);
- kfree(cd);
- return ERR_PTR(ret);
- }
通过这个函数可以看出 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
- static inline int register_chrdev(unsigned int major, const char *name,
- &nb
以上是关于(57)Linux驱动开发之三Linux字符设备驱动的主要内容,如果未能解决你的问题,请参考以下文章