Linux驱动开发基础

Posted 风间琉璃•

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux驱动开发基础相关的知识,希望对你有一定的参考价值。

目录

一、内核态和用户态

二、字符设备驱动

三、Linux设备号

1.设备号的组成

2.设备号的分配

四、模块注册和卸载

五、字符设备注册与注销

六、LICENSE 和作者信息

七、测试指令

1.加载驱动模块

2.创建设备节点文件

3.设备测试

4.卸载驱动模块


一、内核态和用户态

内核态与用户态是操作系统的两种运行级别,cpu提供Ring0-Ring3三种级别的运行模式。Ring0级别最高,Ring3最低。

CPU是在两种不同的模式下运行的:
Kernel Mode(内核态),在内核模式下(执行内核空间的代码),具有ring0保护级别,代码具有对硬件的所有控制权限。可以执行所有CPU指令,可以访问任意地址的内存。内核模式是为操作系统最底层,最可信的函数服务的。在内核模式下的任何异常都是灾难性的,将会导致整台机器停机。

User Mode(用户态),在用户模式下(执行用户空间的代码),具有ring3保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存。它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址。程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存。在这种保护模式下,即时程序发生崩溃也是可以恢复的。在你的电脑上大部分程序都是在用户模式下运行的

用户态和内核态的切换:系统调用。具体详情参考这篇文章:

系统调用

二、字符设备驱动

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。

Linux 下的应用程序调用驱动程序

在Linux下进行驱动开发,完全将驱动程序与应用程序隔开,中间通过C库函数以及系统调用完成驱动层和应用层的数据交换。

在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件应用程序通过对“/dev/xxx” (xxx 是具体的驱动文件名字) 的文件进行相应的操作即可实现对硬件的操作。

比如 /dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。

应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。 open、 close、 write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。

调用open()函数流程

应用程序使用到的函数在具体驱动程序中都有与之对应的函数比如应用程序中调用了 open 函数,那么在驱动程序中也得有一个名为 open 的函数每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。
 

三、Linux设备号

1.设备号的组成

为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面。dev_t 其实是unsigned int 类型,是一个 32 位的数据类型。其中高 12 位为主设备号低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095。
 

设备号的操作函数


MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
 

系统中已经被驱动所使用的的主设备号可以在/proc/devices 文件中查询。

2.设备号的分配

静态设备号:注册字符设备需要给设备指定一个设备号,这个设备号可以是驱动开发者静态指定的设备号。但是需要注意该设备号没有被内核开发者分配掉。使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号。

使用 register_chrdev 函数注册字符设备的时只需要给定一个主设备号即可

/* 注册字符设备驱动 */
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);

这是老版本字符设备注册函数,其0~1048575(2^20-1)这个区间的次设备号全部为设置为0。

动态分配设备号:使用设备号的时向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。

如果没有指定设备号的话就使用如下函数来申请设备号:

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

 如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可

int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数 from 是要申请的起始设备号,也就是给定的设备号

参数 count 是要申请的数量,一般都是一个

参数 name 是设备名字
 

一般采用动态分配设备号,模板如下:

/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (newchrled.major) 
		
    /*  定义了设备号 */
	newchrled.devid = MKDEV(newchrled.major, 0);
	register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
 
else 
		
    /* 没有定义设备号 */
	alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */
	newchrled.major = MAJOR(newchrled.devid);	/* 获取分配号的主设备号 */
	newchrled.minor = MINOR(newchrled.devid);	/* 获取分配号的次设备号 */

printk("newcheled major=%d,minor=%d\\r\\n",newchrled.major, newchrled.minor);	

注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count)

四、模块注册和卸载

Linux 驱动有两种运行方式,第一种是将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。第二种是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用相应命令加载驱动模块。

模块有加载和卸载两种操作,模块的加载和卸载注册函数如下:

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时, xxx_init 这个函数就会被调用

module_exit函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用

五、字符设备注册与注销

当驱动模块加载成功以需要注册字符设备,卸载驱动模块的时也需要注销掉字符设备

static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

register_chrdev 函数用于注册字符设备,unregister_chrdev 注销字符设备。

一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行


六、LICENSE 和作者信息

在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。
 

MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议
MODULE_AUTHOR("mingfei") //添加模块作者信息

七、测试指令

1.加载驱动模块

驱动模块一般挂载在/lib/modules/4.1.15/目录下,所以需要将编写的驱动模块和测试软件复制到根文件系统rootfs/lib/modules/4.1.15 目录下,通过tftp和nfs启动后,在开发板 的/lib/modules/4.1.15 目录下存在驱动模块和测试软件。

驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块: insmod和 modprobe

加载驱动模块:

insmod  xxx.ko

modprobe xxx.ko
 

insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。

但是 modprobe 就不会存在这样的问题, modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中
 

若modprobe 提示无法打开“modules.dep”文件,驱动挂载失败,输入 depmod 命令即可自动生成
modules.dep

lsmod:查看当前系统中存在的模块

cat  /proc/devices : 查看当前系统中所有的设备

2.创建设备节点文件

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作设备节点文件来完成对具体设备的操作。

创建/dev/chrdev设备节点文件

mknod  /dev/chrdev  c  200 0

mknod:创建节点命令
/dev/chrdev:要创建的节点文件
c:字符设备
200 0:设备的主设备号和次设备号

 创建完成后就会存在/dev/chrdev文件,可以使用 ls /dev/chrdev -l命令查看。如果测试软件(make生产的文件)想要读写chrdev设备,直接对/dev/chrdev进行读写操作即可。/dev/chrdev文件是 chrdev设备在用户空间中的实现

3.设备测试
 

运行测试程序即可

 ./xxx(程序)  参数1  参数2...   (参数传给应用层的main函数)

4.卸载驱动模块

如果不再使用某个设备的话可以将其驱动卸载

rmmod   xxx.ko
 

卸载后可以使用lsmod查看卸载模块是否成功。


linux驱动基础开发0——linux 设备驱动概述

linux驱动基础开发0——linux 设备驱动概述 网址:http://blog.csdn.net/xdw1985829/article/details/6800423 目前,Linux软件工程师大致可分为两个层次:

 (1)Linux应用软件工程师(Application Software Engineer):

      主要利用C库函数和Linux API进行应用软件的编写;

      从事这方面的开发工作,主要需要学习:符合linux posix标准的API函数及系统调用,linux的多任务编程技巧:多进程、多线程、进程间通信、多任务之间的同步互斥等,嵌入式数据库的学习,UI编程:QT、miniGUI等。

 (2)Linux固件工程师(Firmware Engineer):

      主要进行Bootloader、Linux的移植及Linux设备驱动程序的设计工作。


一般而言,固件工程师的要求要高于应用软件工程师的层次,而其中的Linux设备驱动编程又是Linux程序设计中比较复杂的部分,究其原因,主要包括如下几个方面:     1)设备驱动属于Linux内核的部分,编写Linux设备驱动需要有一定的Linux操作系统内核基础;需要了解部分linux内核的工作机制与系统组成     2)编写Linux设备驱动需要对硬件的原理有相当的了解,大多数情况下我们是针对一个特定的嵌入式硬件平台编写驱动的,例如:针对特定的主机平台:可能是三星的2410、2440,也可能是atmel的,或者飞思卡尔的等等     3)Linux设备驱动中广泛涉及到多进程并发的同步、互斥等控制,容易出现bug;因为linux本身是一个多任务的工作环境,不可避免的会出现在同一时刻对同一设备发生并发操作     4)由于属于内核的一部分,Linux设备驱动的调试也相当复杂。linux设备驱动没有一个很好的IDE环境进行单步、变量查看等调试辅助工具;linux驱动跟linux内核工作在同一层次,一旦发生问题,很容易造成内核的整体崩溃。

    本系列文章我们将一步步、深入浅出的介绍linux设备驱动编程中设计的一些问题及学习方法,希望对大家学习linux设备驱动有所帮助。

    在任何一个计算机系统中,大至服务器、PC机、小至手机、mp3/mp4播放器,无论是复杂的大型服务器系统还是一个简单的流水灯单片机系统,都离不开驱动程序的身影,没有硬件的软件是空中楼阁,没有软件的硬件只是一堆废铁,硬件是底层的基础,是所有软件得以运行的平台,代码最终会落实到硬件上的逻辑组合。

    但是硬件与软件之间存在一个驳论:为了快速、优质的完成软件功能设计,应用程序工程师不想也不愿关心硬件,而硬件工程师也很难有功夫去处理软件开发中的一些应用。例如软件工程师在调用printf的时候,不许也不用关心信息到底是通过什么样的处理,走过哪些通路显示在该显示的地方,硬件工程师在写完了一个4*4键盘驱动后,无需也不必管应用程序在获得键值后做哪些处理及操作。

    也就是说软件工程师需要看到一个没有硬件的纯软件世界,硬件必须透明的提供给他,谁来实现这一任务?答案是驱动程序,驱动程序从字面解释就是:“驱使硬件设备行动”。驱动程序直接与硬件打交道,按照硬件设备的具体形式,驱动设备的寄存器,完成设备的轮询、中断处理、DMA通信,最终让通信设备可以收发数据,让显示设备能够显示文字和画面,让音频设备可以完成声音的存储和播放。

    可见,设备驱动程序充当了硬件和软件之间的枢纽,因此驱动程序的表现形式可能就是一些标准的、事先协定好的API函数,驱动工程师只需要去完成相应函数的填充,应用工程师只需要调用相应的接口完成相应的功能。无论有没有操作系统,驱动程序都有其存在价值,只是在裸机情况下,工作环境比较简单、完成的工作较单一,驱动程序完成的功能也就比较简单,同时接口只要在小范围内符合统一的标准即可。但是在有操作系统的情况下,此问题就会被放大:硬件来自不同的公司、千变万化,全世界每天都会有大量的新芯片被生产,大量的电路板被设计出来,如果没有一个很好的统一标准去规范这一程序,操作系统就会被设计的非常冗余,效率会非常低。

    所以无论任何操作系统都会制定一套标准的架构去管理这些驱动程序:linux作为嵌入式操作系统的典范,其驱动架构具有很高的规范性与聚合性,不但把不同的硬件设备分门别类、综合管理,并且针对不同硬件的共性进行了统一抽象,将其硬件相关性降到最低,大大简化了驱动程序的编写,形成了具有其特色的驱动组织架构。


下图反映了应用程序、linux内核、驱动程序、硬件的关系。

linux内核分为5大部分:多任务管理、内存管理、文件系统管理、设备管理、网络管理; 每一部分都有承上下的作用,对上提供API接口,提供给应用开发工程师使用; 对下通过驱动程序屏蔽不同的硬件构成,完成硬件的具体操作。

以上是关于Linux驱动开发基础的主要内容,如果未能解决你的问题,请参考以下文章

linux驱动基础开发0——linux 设备驱动概述

嵌入式Linux驱动开发 01:基础开发与使用

Linux驱动开发与Linux嵌入式开发都有哪些相同点和不同点?两者都要学的基础知识都有哪些?

Linux内核开发与Linux驱动开发有啥关系?

Linux驱动基础开发

Linux驱动开发--设备驱动基础笔记 1