目录

一、内核态和用户态

二、字符设备驱动

三、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查看卸载模块是否成功。


Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐