在mini2440上编写linux应用程序
结合前两篇的学习,一个linux操作系统已经在mini2440上运行起来了,结合交叉编译环境和nfs等工具,我们可以在mini2440上编写任何我们在linux系统编程中学到的应用程序。一个简要的多文件Makefile文件如下:
linux驱动程序
linux系统驱动程序分为三大类,字符设备驱动,块设备驱动和网络设备驱动。其中字符设备驱动是使用最多的一种,从点灯到IIC,SPI,音频设备等的驱动都是字符设备驱动。块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都给我们编写好了,大多数情况下都是直接可以使用的。所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、SD 卡和U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。网络设备驱动就更好理解了,就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。一个设备可以属于多种设备驱动类型,比如USB WIFI,由于其使用USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。
我们主要讨论如何编写字符设备驱动。
字符设备驱动程序简介
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI、LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
在详细的学习字符设备驱动架构之前,我们先来简单的了解一下 Linux 下的应用程序是如何调用驱动程序的:
linux系统中万物皆文件,驱动程序加载后会在/dev目录下生成一个对应的文件,如/dev/led。应用程序就是先用open打开该文件,用write控制led的亮灭,用read读取led的亮灭,用完之后用close关闭该文件。
这里需要注意的是,应用程序运行在用户空间,驱动程序运行在内核空间。应用程序必须使用一个叫做**“系统调用”**的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。一个open函数执行的过程如下:
字符设备驱动程序的编写
linux源码中字符设备驱动程序存放在driver/char目录下,我们也可以将我们自己的驱动程序保存在该目录下
在driver/char下创建源文件first_driver.c并在文件中填入如下代码:
linux驱动程序对代码有着特定的要求,首先就是驱动程序加载和卸载时的函数调用,48和53行分别就是当驱动程序加载和卸载时调用的函数。57和58行分别用两个带参宏指出驱动程序初始化入口点和退出入口点。59表示该驱动程序遵守的协议。这样就完成了驱动程序最基础的框架。
目前我们编写的是一个字符设备驱动程序,字符设备驱动程序将来被linux加载的时候需要注册这个驱动程序。其实无论哪种驱动程序,按照linux的做法在加载时都需要注册。字符设备驱动程序注册函数为:
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
int register_chrdev函数
1.unsigned int major:主设备号,这里就不得不提一下linux中的设备号了。一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的设备。简单来说,linux需要一个数来管理某个驱动程序和使用这个驱动程序的设备。很明显,这个设备号具有唯一性。我们可以使用cat /proc/devices命令即可查看当前系统中所有已经使用了的设备号。在接下来的程序中,我们可以设置一个静态的主设备号,比如200。设置时一定要注意不能使用已经用了的主设备号。
2. const char name:为你的驱动程序起一个名字
3. struct file_operations fops:这是一个指向file_operations结构体变量的指针**,这个结构体里面的成员绝大多数都是函数的指针。这些函数的指针指向一个我们编写的函数,每个函数都有着各自的作用。这里列举常用的几个:
① open 函数用于打开设备文件
② release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应
③ read 函数用于读取设备文件
④ write 函数用于向设备文件写入(发送)数据
⑤ poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写
⑥ owner 拥有该结构体的模块的指针,一般设置为THIS_MODULE
现在我们的方向已经非常明确了,为了调用注册字符设备驱动函数,不得不先准备一个file_operations结构体变量,而这个结构体变量中必要的成员,必须提前准备几个函数。
第12和13行:为了将来能看出这个函数调用了,输出一些信息,由于我们的程序现在运行在内核空间,所以不能用printf而是要用printk,用法和printf一样;
第19行:使用copy_to_user函数把内核空间的数据拷贝到用户空间;
然后定义一个file_operations结构体变量,并将函数入口地址赋字符设备驱动值给各自的成员:
下面就可以调用register_chrdev函数注册字符设备驱动了。
#define DEVICE_MAJOR 200
#define DEVICE_NAME "first_device"
注册主设备号为200,名字为first_device的字符设备驱动。
程序写好了,如何编译呢?这里介绍两种方法
第一种方法是告诉linux的Makefile我们添加了一个新的驱动程序,这种方法需要我们的驱动源码就放在driver/char目录中,恰好我们就是这么做的。步骤如下:
第一种方法
- 打开 drivers/char/Kconfig 文件并添加如下内容:
Kconfig文件被称之为内核配置文件,这里我们添加了一个名为FIRST_DRIVER的配置选项,该配置选项为三态的,所谓三态是指将来的编译结果可以是模块,可以直接编译进内核还可以不编译。default m是指默认编译成模块。最后那段是帮助文本。
之后我们运行make menuconfig,依次进入Device Driver-Character devices就可以看到My first driver了。
选择该项按空格分别在编译成模块,编译进内核和不编译之间切换。我们选择’M’,编译成模块,以方便之后的调试。 - 通过上一步,我们虽然可以在配置内核的时候进行选择,但实际上此时执行编译内核还是不能把first_dirver.c 编译进去的,还需要在 Makefile 中把内核配置选项和真正的源代码联系起来。打开 drivers/char/Makefile并添加下面内容:
注意:CONFIG_之后的文字必须和Kconfig文件中配置选项名称一致,之后的文件名也必须和驱动源码文件名一致。 - 之后在源码顶层目录下执行make modules就可以完成编译了
编译完成之后在driver/char目录下可以找到一个名为first_driver.ko文件,这个就是我们需要的驱动程序
将first_driver.ko通过nfs复制到开发板上
使用insmod xxx.ko命令加载驱动程序
使用lsmod查看已经加载的驱动
使用rmmod xxx卸载驱动程序,注意不用加.ko
加载好驱动程序以后,查看一下/dev目录,这里并没有出现我们所说的设备文件,这是因为目前我们的驱动程序还不能自动创建设备文件,之后我们会讲解如何自动创建设备文件。这里我们先用手动的方法创建设备文件,命令为mknod [OPTIONS] NAME TYPE MAJOR MINOR
该命令中**[OPTIONS]选项可不填**;NAME就是/dev下的设备文件名;TYPE是设备型号,这里是字符设备用c表示;MAJOR主设备号;MINOR子设备号。
如:mknod /dev/first c 200 0那么以后应用程序就是以文件/dev/first作为入口点调用驱动程序的。
第二种方法
将我们写好的驱动源码放在任意一个文件夹内,如linux源码目录下的mydriver目录,并在该目录下创建一个Makefile文件,内容如下:
第1行:获得linux源码顶层目录,根据实际情况填写
第2行:获得驱动源码所在目录
第3行:定义目标文件并指定目标文件为模块形式
第8行:具体的编译命令,后面的 modules 表示编译模块,-C 表示将当前的工作
目录切换到指定目录中,也就是 KERNERLDIR 目录。M 表示模块源码目录,”make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件
之后使用make命令编译同样得到xxx.ko文件,加载什么的操作就和之前一样了。
正如之前所提到的,驱动程序的调用是通过应用程序的文件IO实现的。所以调用驱动程序就是编写一个简单的文件IO程序。
第11行:打开设备文件
第22行:调用read函数,此时就会调到我们在驱动中写的first_driver_read函数
可以看到的结果演示如下: