从单片机到ARM Linux驱动——Linux驱动入门篇

       大一到大二这段时间里学习过单片机的相关知识,对单片机有必定的认识和了解。若是要深究其原理可能还差了一些火候。知道如何编写程序来点量一个LED灯,改一改官方提供的例程来实现一些功能作一些小东西,对IIC、SPI底层的通讯协议有必定的了解,可是学着学着逐渐以为单片机我也就只能改改代码了(固然有的代码也不必定能改出来)。对于我这种之后不想从事单片机开发想搬砖的码农来讲已经差很少了(仅仅是我的观点)。
       在单片机开发中咱们经常用到的是裸机,并无用到操做系统(或者接触过ucos/rtos这种实时操做系统),可是嵌入式Linux开发就必须得在Linux系统中进行操做。咱们须要熟悉Linux操做系统,知道Linux的经常使用命令、文件系统、Linux网络、多线程/多进程,同时要会用vi编辑器、gcc编译器、shell脚本和一些简单的makefile的编写,在这些的基础之上进行Linux驱动开发的学习就会如步青云。node

往期推荐:
       史上最全的Linux经常使用命令汇总(超全面!超详细!)收藏这一篇就够了!
       STM32经过PWM产生频率为20HZ占空比为50%方波,并经过单片机测量频率并显示在这里插入图片描述linux

       嵌入式Linux操做系统具备:开放源码、所需容量小(最小的安装大约须要2MB)、不需著做权费用、成熟与稳定(经历这些年的发展与使用)、良好的支持等特色。所以被普遍应用于移动电话、我的数码等产品中。嵌入式Linux开发主要包括:底层驱动、操做系统内核、应用开发三大类。须要掌握系统移植(Uboot、Linux Kernel的移植和裁剪、根文件系统的构建)、Linux驱动及内核开发(字符设备驱动、块设备驱动、网络设备驱动)应用开发因为博主能力有限所了解的也很少。web

字符设备驱动简介

       字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个字节,按照字节进行读写操做设备,读写数据是分前后顺序的。好比咱们常见的点灯、按键、IIC、SPI、LCD等都是字符设备,这些设备的驱动就叫作字符设备驱动。
       在Linux中开发通常只能是用户态,也就是用户只能编写应用程序,可是要做用于内核,那么就须要了解Linux中应用程序是如何调用内核中的驱动程序的,Linux 应用程序对驱动程序的调用以下图所示:
在这里插入图片描述
       在Linux 中一切皆为文件,驱动加载成功之后会在“/dev”目录下生成一个相应的文件,应用程序经过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操做便可实现对硬件的操做。好比如今有个叫作/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open 函数来打开文件/dev/led,使用完成之后使用 close 函数关闭/dev/led 这个文件。 open和 close 就是打开和关闭 led 驱动的函数,若是要点亮或关闭 led,那么就使用 write 函数来操做,也就是向此驱动写入数据,这个数据就是要关闭仍是要打开 led 的控制参数。若是要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。
       应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,所以驱动运行于内核空间。当咱们在用户空间想要实现对内核的操做,好比使用 open 函数打开/dev/led 这个驱动,由于用户空间不能直接对内核进行操做,所以必须使用一个叫作“系统调用”的方法来实现从用户空间陷入到内核空间,这样才能实现对底层驱动的操做。 open、 close、 write 和 read 等这些函数是有 C 库提供的,在 Linux 系统中,系统调用做为 C 库的一部分。当咱们调用 open 函数的时候流程如图所示:
在这里插入图片描述
       应用程序使用到的函数在具体的驱动中都有与之对应的函数,好比应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫作 file_operations 的结构体,此结构体就是 Linux 内核驱动操做函数集合shell

struct file_operations { 
 
  
	struct module *owner;//owner 拥有该结构体的模块的指针,通常设置为 THIS_MODULE
	loff_t (*llseek) (struct file *, loff_t, int);//llseek 函数用于修改文件当前的读写位置
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t*);//read 函数用于读取设备文件
	ssize_t (*write) (struct file *, const char __user *, size_t,loff_t *);//write 函数用于向设备文件写入(发送)数据
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct*);//poll 是个轮询函数,用于查询设备是否能够进行非阻塞的读写
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);//compat_ioctl 函数与 unlocked_ioctl 函数功能同样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
	int (*mmap) (struct file *, struct vm_area_struct *);//mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),通常帧缓冲设备会使用此函数,好比 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中之后应用程序就能够直接操做显存了,这样就不用在用户空间和内核空间之间来回复制。
	int (*mremap)(struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);//open 函数用于打开设备文件。
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);//release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);//fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
	int (*aio_fsync) (struct kiocb *, int datasync);//aio_fsync 函数与 fasync 函数的功能相似,只是 aio_fsync 是异步刷新待处理的数据
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	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);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,
	loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct
	pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
	#ifndef CONFIG_MMU
		unsigned (*mmap_capabilities)(struct file *);
	#endif
};

字符设备驱动开发步骤

       在学习裸机或者 STM32 的时候关于驱动的开发就是初始化相应的外设寄存器,在 Linux 驱动开发中确定也是要初始化相应的外设寄存器,这是毫无疑问的。只是在 Linux 驱动开发中咱们须要按照其规定的框架来编写驱动,因此说学 Linux 驱动开发重点是学习其驱动框架api

驱动模块的加载和卸载

       Linux 驱动有两种运行方式第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动之后使用“insmod”命令加载驱动模块。在调试驱动的时候通常都选择将其编译为模块,这样咱们修改驱动之后只须要编译一下驱动代码便可,不须要编译整个 Linux 代码。并且在调试的时候只须要加载或者卸载驱动模块便可,不须要重启整个系统。bash

       模块有加载和卸载两种操做,咱们在编写驱动的时候须要注册这两种操做函数,模块的加载和卸载注册函数以下:网络

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 int __init xxx_init(void)
{ 
 
  
	/* 入口函数具体内容 */
	return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{ 
 
  
	 /* 出口函数具体内容 */
 }

	/* 将上面两个函数指定为驱动的入口和出口函数 */
	module_init(xxx_init);
	module_exit(xxx_exit);
  • 第 2 行,定义了个名为 xxx_init 的驱动入口函数,而且使用了“__init”来修饰。
  • 第 9 行,定义了个名为 xxx_exit 的驱动出口函数,而且使用了“__exit”来修饰。
  • 第 15 行,调用函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init函数就会被调用。
  • 第16行,调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用。

       驱动编译完成之后扩展名为.ko,有两种命令能够加载驱动模块: insmodmodprobe,insmod是最简单的模块加载命令,此命令用于加载指定的.ko 模块,好比加载 drv.ko 这个驱动模块,命令以下:app

insmod drv.ko

       insmod 命令不能解决模块的依赖关系,好比 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,而后再加载 drv.ko 这个模块。可是 modprobe 就不会存在这个问题, modprobe 会分析模块的依赖关系,而后会将全部的依赖模块都加载到内核中,所以modprobe 命令相比 insmod 要智能一些。 modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。 modprobe 命令默认会去/lib/modules/目录中查找模块,好比本书使用的 Linux kernel 的版本号为 4.1.15,所以 modprobe 命令默认到/lib/modules/4.1.15 这个目录中查找相应的驱动模块,通常本身制做的根文件系统中是不会有这个目录的,因此须要本身手动建立。驱动模块的卸载使用命令“rmmod”便可,好比要卸载 drv.ko,使用以下命令便可:框架

rmmod drv.ko

       也可使用“modprobe -r”命令卸载驱动,好比要卸载 drv.ko,命令以下:

modprobe -r drv.ko

       使用 modprobe 命令能够卸载掉驱动模块所依赖的其余模块,前提是这些依赖模块已经没有被其余模块所使用,不然就不能使用 modprobe 来卸载驱动模块。因此对于模块的卸载,仍是推荐使用 rmmod 命令。

字符设备注册与注销

       对于字符设备驱动而言,当驱动模块加载成功之后须要注册字符设备,一样,卸载驱动模块的时候也须要注销掉字符设备。字符设备的注册和注销函数原型以下所示:

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 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义以下:
  • major: 主设备号, Linux 下每一个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
  • name:设备名字,指向一串字符串。
  • fops: 结构体 file_operations 类型指针,指向设备的操做函数集合变量。
  • unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义以下:
  • major: 要注销的设备对应的主设备号。
  • name: 要注销的设备对应的设备名。

通常字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。在下面代码中字符设备的注册和注销,内容以下所示:

static struct file_operations test_fops;

/* 驱动入口函数 */
static int __init xxx_init(void)
{ 
 
  
	/* 入口函数具体内容 */
	int retvalue = 0;

	/* 注册字符设备驱动 */
	retvalue = register_chrdev(200, "chrtest", &test_fops);
	if(retvalue < 0){ 
 
  
		/* 字符设备注册失败,自行处理 */
	}
	return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{ 
 
  
	/* 注销字符设备驱动 */
	unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
  • 以上代码中,一开始定义了一个 file_operations 结构体变量 test_fops, test_fops 就是设备的操做函数集合,只是此时咱们尚未初始化 test_fops 中的 open、 release 等这些成员变量,因此这个操做函数集合仍是空的。
  • 第十行,调用函数 register_chrdev 注册字符设备,主设备号为 200,设备名字为“chrtest”,设备操做函数集合就是第 1 行定义的 test_fops。要注意的一点就是,选择没有被使用的主设备号,输入命令cat /proc/devices能够查看当前已经被使用掉的设备号。
  • 第二十一行,调用函数 unregister_chrdev 注销主设备号为 200 的这个设备。

实现设备的具体操做函数

       file_operations 结构体就是设备的具体操做函数,在示例代码 40.2.2.1 中咱们定义了file_operations结构体类型的变量test_fops,可是还没对其进行初始化,也就是初始化其中的open、release、 read 和 write 等具体的设备操做函数。本节小节咱们就完成变量 test_fops 的初始化,设置好针对 chrtest 设备的操做函数。在初始化 test_fops 以前咱们要分析一下需求,也就是要对chrtest 这个设备进行哪些操做,只有肯定了需求之后才知道咱们应该实现哪些操做函数。假设对 chrtest 这个设备有以下两个要求:
一、可以对 chrtest 进行打开和关闭操做
       设备打开和关闭是最基本的要求,几乎全部的设备都得提供打开和关闭的功能。所以咱们须要实现 file_operations 中的 open 和 release 这两个函数。
二、对 chrtest 进行读写操做
       假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序须要经过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操做。因此须要实现 file_operations 中的 read 和 write 这两个函数。需求很清晰了,修改驱动示例代码在其中加入 test_fops 这个结构体变量的初始化操做,完成之后的内容以下所示:

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{ 
 
  
	/* 用户实现具体功能 */
	return 0;
}
/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{ 
 
  
	/* 用户实现具体功能 */
	return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp,
const char __user *buf,
size_t cnt, loff_t *offt)
{ 
 
  
	/* 用户实现具体功能 */
	return 0;
}
/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{ 
 
  
	/* 用户实现具体功能 */
	return 0;
}

static struct file_operations test_fops = { 
 
  
	.owner = THIS_MODULE,
	.open = chrtest_open,
	.read = chrtest_read,
	.write = chrtest_write,
	.release = chrtest_release,
};

/* 驱动入口函数 */
static int __init xxx_init(void)
{ 
 
  
	/* 入口函数具体内容 */
	int retvalue = 0;
	
	/* 注册字符设备驱动 */
	retvalue = register_chrdev(200, "chrtest", &test_fops);
	if(retvalue < 0){ 
 
  
		/* 字符设备注册失败,自行处理 */
	}
	return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{ 
 
  
	/* 注销字符设备驱动 */
	unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
  • 在上面代码中,咱们一开始编写了四个函数:chrtest_openchrtest_readchrtest_writechrtest_release。这四个函数就是 chrtest 设备的 open、 read、 write 和 release 操做函数。第 29行~35 行初始化 test_fops 的 open、read、 write 和 release 这四个成员变量。

添加LICENSE和做者信息

       在驱动编写最后,咱们须要在驱动中加入LICENSE信息和做者信息,其中LICENSE是必须添加的,不然的话编译时会报错,做者信息能够添加也能够不添加。 LICENSE 和做者信息的添加使用以下两个函数:

MODULE_LICENSE() //添加模块 LICENSE 信息
	MODULE_AUTHOR() //添加模块做者信息

       给示例代码加入 LICENSE 和做者信息,完成之后的内容以下:

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{ 
 
  
	/* 用户实现具体功能 */
	return 0;
}
......

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

MODULE_LICENSE("GPL");//LICENSE 采用 GPL 协议。
MODULE_AUTHOR("wly");//添加做者名字

       当添加完做者和LICENSE和做者信息后,字符设备驱动的完整流程就基本上结束了,而且也提供了一个完整的Linux驱动的模板,之后字符设备驱动开发就能够修改这个模板。

Linux设备号

       Linux的设备管理是和文件系统紧密结合的,各类设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序能够打开、关闭和读写这些设备文件,完成对设备的操做,就像操做普通的数据文件同样。为了管理这些设备,系统为设备编了号,这个号就被称为Linux设备号!

设备号的组成

       设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件include/linux/types.h 里面,定义以下:

typedef __u32 __kernel_dev_t;
......
typedef __kernel_dev_t dev_t;

       能够看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h 里面,定义以下:

typedef unsigned int __u32;

       dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,第 20 位为次设备号。所以 Linux系统中主设备号范围为0~4095,因此你们在选择主设备号的时候必定不要超过这个范围。在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操做函数(本质是宏),以下所示:

#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))
  • 第 1 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
  • 第 2 行,宏 MINORMASK 表示次设备号掩码。
  • 第 3 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位便可。
  • 第 4 行,宏 MINOR 用于从 dev_t 中获取此设备号,取 dev_t 的低 20 位的值便可。
  • 第 5 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

设备号的分配

一、静态分配设备号

注册字符设备的时候须要给设备指定一个设备号,这个设备号能够是驱动开发者静态的指定一个设备号,好比选择 200 这个主设备号。有一些经常使用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容能够查看文档 Documentation/devices.txt。并非说内核开发者已经分配掉的主设备号咱们就不能用了,具体能不能用还得看咱们的硬件平台运行过程当中有没有使用这个主设备号,使用cat /proc/devices命令便可查看当前系统中全部已经使用了的设备号。

二、动态分配设备号

静态分配设备号须要咱们检查当前系统中全部被使用了的设备号,而后挑选一个没有使用的。并且静态分配设备号很容易带来冲突问题, Linux 社区推荐使用动态分配设备号,在注册字符设备以前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号便可,设备号的申请函数以下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
  • dev:保存申请到的设备号。
  • baseminor: 次设备号起始地址, alloc_chrdev_region 能够申请一段连续的多个设备号,这些设备号的主设备号同样,可是次设备号不一样,次设备号以 baseminor 为起始地址地址开始递增。通常 baseminor 为 0,也就是说次设备号从 0 开始。
  • count: 要申请的设备号数量。
  • name:设备名字。

注销字符设备以后要释放掉设备号,设备号释放函数以下:

void unregister_chrdev_region(dev_t from, unsigned count)
  • from:要释放的设备号。
  • count: 表示从 from 开始,要释放的设备号数量。

       不积小流无以成江河,不积跬步无以致千里。而我想要成为万里羊,就必须坚持学习来获取更多知识,用知识来改变命运,用博客见证成长,用行动证实我在努力。
       若是个人博客对你有帮助、若是你喜欢个人博客内容,记得“点赞” “评论” “收藏”一键三连哦!据说点赞的人运气不会太差,每一天都会元气满满呦!若是实在要白嫖的话,那祝你开心每一天,欢迎常来我博客看看。
在这里插入图片描述