底层驱动编写的入门

1.

安装source lnsight3.5,选择文件夹存放生成的文件(自己创建)

 然后将选择需要使用的源文件,分为两步,这里用到linux3.14是linux的内核源码,可以在网上进行下载

 

 

 

我们只添加内核驱动需要的文件,其他文件也可以添加,但没必要,运行比较吃性能,如果你电脑好就当我没说,你可以添加整个linux3.14 。完成以上就完成了基本的环境安装。

(建立索引(索引的同步),这一步是为了让有的函数,结构体,变量可以通过快捷方式找到)

2.

在底层驱动开发时,要先了解什么是驱动。早些年WindowsXP有个许许多多小毛病,我到现在还记忆犹新,玩穿越火线时键盘失灵,不亮灯。哪怕更换键盘也不行,于是百度搜索解决问题,大多是的回答是重新加载驱动(虽然后面是通过换usb键盘解决的)。这是我第一次接触驱动这个词。

驱动就是一个设备,比如鼠标,键盘,扬声器,麦克风,网卡等等。那么底层驱动的开发就是通过代码将硬件激活,然后交给应用层的兄弟去完成对应功能。

在大学时候最早学习的驱动应该就是51单片机的点灯,这就是驱动,但是和现在的“驱动”有着一些差异,51的点灯是裸机驱动,现在大部分都是系统驱动(linux驱动)。裸机驱动是通过直接操作寄存器,完成对功能实现,但是linux驱动是通过书写一个接口,应用层通过这个接口完成逻辑上的应用程序。在linux的根目录下的dev文件夹下可以看见很多设备文件(设备抽象成设备文件对其操作)

linux驱动大致分为三类:

  1. 字符设备:

    字符(char)设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。字符设备驱动程序通常至少要实现open、close、read和write的系统调用。字符终端(/dev/console)和串口(/dev/ttySO以及类似设备)就是两个字符设备,它们能很好的说明“流”这种抽象概念。字符设备可以通过FS节点来访问,比如/dev/tty1 和/dev/lp0等。这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道。然而,也存在具有数据区特性的字符设备,访问它们时可前后移动访问位置。例如framebuffer就是这样的一个设备,app可以用mmap或lseek访问抓取的整个图像。

  2. 块设备:

    和字符设备类似,块设备也是通过/dev目录下的文件系统节点来访问。块设备(例如磁盘)上能够容纳filesystem。在大多数的Unix系统中,进行I/O操作时块设备每次只能传输一个或多个完整的块,而每块包含512字节(或2的更高次幂字节的数据)。Limux可以让app像字符设备一样地读写块设备,允许一次传递任意多字节的数据。因此,块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的软件接口,而这些不同对用户来讲是透明的。在内核中,和字符驱动程序相比,块驱动程序具有完全不同的接口。

  3. 网络设备:

    网络设备不存在dev目录下,我们可以通过ifconfig查看网卡信息,我们可以看到两个一个是ens33(真是网卡),一个是lo(测试网卡应用层,本地回环接口),任何网络事物都需要经过一个网络接口形成,网络接口是一个能够和其他主机交换数据的设备。接口通常是一个硬件设备,但也可能是个纯软件设备,比如回环(loopback)接口。网络接口由内核中的网络子系统驱动,负责发送和接收数据包。许多网络连接(尤其是使用TCP协议的连接)是面向流的,但网络设备却围绕数据包的传送和接收而设计。网络驱动程序不需要知道各个连接的相关信息,它只要处理数据包即可。由于不是面向流的设备,因此将网络接口映射到filesystem中的节点(比如/dev/tty1)比较困难。Unix访问网络接口的方法仍然是给它们分配一个唯一的名字(比如ethO),但这个名字在filesystem中不存在对应的节点。内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包相关的函数而不是read、write等。

3.

我们直接上代码写一个最简陋的驱动,为什么说是简陋,因为我们不实现任何功能,只是先了解框架,在之后的学习中往里面补充

#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("GPL v2");

static int led_init(void)
{
	printk("led init\n");
	return 0;
}

static void led_exit(void)
{
	printk("led exit\n");
}

module_init(led_init);
module_exit(led_exit);


我们从头往下看:

头文件看名字不难理解,一个初始化相关头文件,一个是模块相关头文件。为什么会出现模块呢?这里就要引入一个概念,linux内核的中驱动代码的思想是什么。很多人可能会想到,linux不是C写的吗,那肯定是面向过程的思想。但是这个答案完全错误,linux内核驱动的代码思想是面向过程的。将一个个驱动设备(linux万物皆文件,也意味着将驱动设备理解为驱动文件),封装成一个个模块,通过模块的增删改查完成对驱动的更改。但是要意识到一个问题,就是C没有像C++的继承方法。那就只能通过结构体中放结构体的嵌套方法去完成。我们可以回想一下,在C中有哪些调用内核驱动的函数,比如说文件IO中的open对吧。它的实现原理就是应用层,通过函数指针open调用到了打开文件相关的驱动。应用层只负责调用,他不关心具体实现。

MODULE_LICENSE("GPL v2");

这一句的意义在于你后面调用linux相关函数,结构体时是否能正常使用,这句代码的目的就是要求你遵循GPL规则,这个规则就是说你在完成驱动编写后,需要开源(这是linux开发者规定的),但是我们学习无所谓,具体怎么开源是由公司去帮忙完成的,我们只需要记得加上这一句就行。

我们注意到两个函数的输出,是使用的printk 而不是printf,那是因为内核输出是printk,用户输出是printf。用法是一样的没有什么别的区别。

最后两条语句是向内核说明驱动入口,和驱动出口。进去驱动时会调用led_init函数,驱动退出时会调用led_exit函数。

以上是最基本的框架,缺一不可,并且还要注意入口函数和出口函数的参数和返回值,是固定的不能改变。

4.

我们完成了.c文件的编写,我们运行一下看看能不能编译。发现一个问题就是在编译的时候出错了

显示找不到头文件,那么我们用gcc 文件 -v 编译,“-v”可以查看编译时查找头文件的目录

也就意味着这里的路径下找不到对应的头文件,显示查找路径找的X86下运行代码的头文件,而我们是内核运行代码的头文件,所以找不到是必然的,所以需要去内核空间去找。那么解决方法也很多,可以自己在gcc后面添加后缀,让他去搜索 ,或者通过写个Makefile去编译,在Makefile中完成查找,通过内核的编译系统去编译。我们一定是使用第二种方法,因为后续还需要其他文件相互配合编译,第一种方法太过鸡肋。那么Makefile怎么写呢?怎么找到内核的编译系统呢?

Ubuntu自带的内核的编译系统位置:linux/modules/版本文件/build/Makefile (你想要你的文件在哪个平台跑,就用对应的编译系统,不能内核文件用X86的编译系统编译)

我们通过他的Makefile编译我们自己的代码,他的Makefile会帮我们找到头文件,他已经写好了。我们可以通过uname -r去查看自己当前的版本文件是哪个,我们直接上Makefile代码

make -C:他的作用是改变路径,将路径改变到编译系统的路径下去。这里C是change的意思。

M=:是需要编译的模块代码路径

$(shell uname -r):表示获取执行shell命令之后的值

module:表示执行make的时候后缀加module(make module),这里不是乱写的,因为内核中也是写的module

clean:是清除编译生成的文件

ifeq:为什么要比较呢?相当于C中的条件编译,这里是看一下这个KERELRELEASE的值是不是为空,如果是空执行后面的语句,如果不是执行else

else:你找内核之后,内核再调用一次我们的Makefile回去找哪个文件要编译,因为内核中的KERELRELEASE是定义了的,会走else这条路。

所以这里的核心思想是通过自己的Makefile,去找内核的Makefile,然后内核的Makefile在找自己的目录确定编译文件是谁,帮助你完成编译,流程图如下:(可以第一次走亲家类比记忆)

 写完再编译就可以使用了,注意看出生成了哪些文件。

5.模块操作

通过这些命令可以实现对模块的调用,没实现具体功能,只能在内核空间查看输出信息。

6.

我们完成了模块的编写,那么接下来就要考虑到如何让应用层的人可以完成对模块调用呢?

我们上面也说了,应用层是通过函数指针去调用接口的,我们实现的驱动也就是个接口。所以要将我们所写的模块,加入到对应的结构体中,这里也就是内核设计中的面向对象思想。我们还没有提供接口所以只是一个模块代码,而不是个驱动代码。我们通过一张图了解如何完成接口的设计:

 我们发现内核提供的函数接口是放在一个结构体file_operations中,而且还是以函数指针的形式,为什么是函数指针形式,不是其他任何指针。因为函数指针对于返回值,参数是由要求的。那这个函数指针具体指向什么函数,就由我们去实现了。(C++中的多态(虚函数表内的指针),线程函数,信号处理函数这三个也是函数指针去调用的)

然后这个驱动的结构体就放到结构体cdev中,cdev是存放字符设备的结构体,cdev中放设备结构体,设备结构体中又放了这个设备的函数,不就完成了结构体中,放结构体的面向对象思想。我们直接上完整代码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>

MODULE_LICENSE("GPL v2");

struct led_device
{
	struct cdev led_cdev;//这个结构体是内核提供,用来存放字符设备结构体地址的
};

struct led_device *pled;

static int led_chrdev_open(struct inode *inode, struct file *file)
{
	printk("led chrdev open\n");
	return 0;
}

static int led_chrdev_release(struct inode *inode, struct file *file)
{
	printk("led chrdev release\n");
	return 0;
}

static const struct file_operations led_fops = {
	.owner   = THIS_MODULE,
	.open    = led_chrdev_open,
	.release = led_chrdev_release,
};

static int led_init(void)
{
	printk("led init\n");

	pled = kmalloc(sizeof(*pled), GFP_KERNEL);
	if(!pled)
		{
			return -ENOMEM;
		}
	//pled->led_cdev.ops = &led_fops;//最好采用下面它给你写好的方法,因为还有其他的东西需要初始化
	cdev_init(&pled->led_cdev, &led_fops);
	
	return 0;
}

static void led_exit(void)
{
	printk("led exit\n");

	kfree(pled);
}

module_init(led_init);
module_exit(led_exit);


这里结构体怎么写,要会抄。抄别人驱动的代码。

7.注册设备号并添加设备文件

我们以及完成了驱动设备的接口编写,那应用层如何找到这个接口呢?答案是通过设备号。我们观察dev下的文件信息和普通文件对比发现,驱动设备有设备号,但没有内存大小,分为一个主设备号和一个次设备号,就好比学号一样,班级号码一样,但个人号码不一样,图片如下:

那我们如何注册设备号呢,直接看代码:

具体注册函数的用法,MKDEV函数的用法直接去查看源代码,并结合他人的代码进行推断。

编译后,启动驱动,通过cat /proc/devives查看到对应的设备号。

我们现在既然以及有了设备号,那么还需要根据设备号添加字符设备(前面的图中有)有了设备才能进行访问。

 我们添加完字符设备,开启驱动后,在dev目录下还是找不到212这个设备号,这里需要手动创建设备文件关联我们的字符设备

 然后就可以应用层就可以通过打开dev目录下设备文件调用到我们的字符设备

然后运行查看一下我们的内核打印的信息

到这里已经完成了阶段性的实现,赶快去试试吧。

总结:

到这里为止,算是编写驱动的一个入门,我们总结一下整体的流程和为什么是这样的流程。

首先linux驱动的本质就是写给应用层的函数接口,应用层通过接口可以实现对硬件的控制。那就需要先完成驱动的编写,然后考虑如何将驱动和应用层相互关联。驱动的编写的框架就是上面的第三点。完成了驱动的编写之后就考虑到如何关联。应用层在打开文件后会找到inode结构体(第七步创建设备文件时就生成了),一个文件就对应着这样的一个结构体,通过结构体中的文件类型信息,可以找到对应的字符设备或者其他设备和文件,再通过inode结构体中断设备号信息可以找到对应的cdev数组(0-254大小的数组),这个数组中就记录了设备号再结合inode的设备号找到对应的cdev结构体,那为什么会记录呢,因为第七步就添加进去了。那这个cdev结构体中又存放了一个结构体指针(指向函数指针结构体),再通过函数指针结构体不就调用到了对应的函数吗。


8.

我们完成了在虚拟机的Ubuntu上(也可以说是X86)的驱动编写以及运行,那么作为一个嵌入式驱动,肯定还要满足可以在ARM或者其他平台运行的条件。其实很简单,只需要根据前面的所说的找到对应的编译系统,通过对应ARM环境的编译系统进行编译就行,我这边是有一个现成已经配置好的编译系统,针对ARM平台三星芯片的一个编译系统,我们更改Makefile,实现编译

 我cp的位置是因为我下载到开发板上是通过nfs网络方式挂载文件系统,就是cp到挂载目录下了。

9.

我们然后在开发板去运行程序,剩下的就是完成具体实现,我们就用最简单的点灯来说,就和裸机驱动没有差别了,直接在函数中操作寄存器就行。

有很多地方说的其实并不清楚,你既然能看到这里说明你足够想学了,很多问题我由于时间关系篇幅原因,没有详细解答,如果你需要我的帮助大胆评论就行,我也是入门新手,我们一起解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值