2024年5月2日发(作者:)

嵌入式Linux驱动开发(一)——字符设备驱动框架

入门

提到了关于Linux的设备驱动,那么在Linux中I/O设备可以分为两类:块设备和

字符设备。这两种设备并没有什么硬件上的区别,主要是基于不同的功能进行了分

类,而他们之间的区别也主要是在是否能够随机访问并操作硬件上的数据。

1. 字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存

取。相反,此类设备支持按字节/字符来读写数据。举例来说,调制解调器

是典型的字符设备。

2. 块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。

硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数

据。此外,数据的读写只能以块(通常是512Byte)的倍数进行。与字符设备

不同,块设备并不支持基于字符的寻址。 两种设备本身并没用严格的区分,

主要是字符设备和块设备驱动程序提供的访问接口(file I/O API)是不一样

的。本文主要就数据接口、访问接口和设备注册方法对两种设备进行比较。

那么,首先,认识一下字符设备的驱动框架。

对于上层的应用开发人员来说,没有必要了解具体的硬件是如何组织在一起并工作

的。比如,在Linux中,一切设备皆文件,那么应用程序开发者,如果需要在屏幕

上打印一串文字,虽然表面看起来只是使用printf函数就实现了,其实,他也是

使用了int fprintf(FILE *fp, const char* format[, argument,...])封装后的结果,而实

际上,fprintf函数操作的还是一个FILE,这个FILE对应的就是标准输出文件,也

就是我们的屏幕了。

那么最简单的字符设备驱动程序的框架是如何呢?

应用程序和底层调用的结构

正如上图所显示的那样,用户空间的应用开发者,只需要通过C库来和内核空间

打交道;而内核空间通过系统调用和VFS(virtual file system),来调用各类硬件

设备的驱动。如果,有过单片机的经验,那么一定知道,操作硬件简单来说就是操

作对应地址的寄存器中的内容。而硬件驱动实际就是和这些寄存器打交道的。通过

操作对应硬件的寄存器来直接的控制硬件设备。

那么,对于上面这幅图可以看出,驱动程序实际也是内核的一部分,当然可以把代

码直接放到内核中一起编译出来。但是对于很多开发板来说,内核来说早已经编译

完成运行在开发板上,那么是不是必须要重新编译并烧写整个内核呢? 换到我们

使用pc来说,显然不是这样,如果我们购买了一个键盘,为了键盘还需要重新安

装对应的操作系统,那么未免也太不方便,并且我们的使用经验也并非如此。 而

在之前谈到的内核编译过程中,可以将一些模块编译为module的方式编译,在运

行时加载该模块即可,而不用每次都需要完整的对内核进行编译。 因此,对于驱

动程序的开发来说,这一点就显得很重要,也是我们日常工作最常用的一种方式。

那么我们先回顾一下,在应用层我们一般是如何来操作一个设备文件的?我们通常

会使用一些类似于read、open、write等函数。(可以参见我之前写的文章:)。

那么在使用这些函数的时候,会包含一些头文件,例如:sys/types.h、sys/stat.h

以及fcntl.h等这些头文件,实际他们就是C库的部分,用户程序这时候只需要关

心的是C库到底如何使用,而C库背后实际完成的是调用一些系统调用,类似于

sys_open、sys_read等函数来对内核空间进行调用的。

在这里毕竟不是为了分析框架的具体实现原理,以后有机会慢慢展开,在此主要为

了讨论,如何快速使用这些框架来写出字符设备的驱动程序。

其实编写字符驱动的步骤并不复杂,我们首先将框架建立起来,建立框架的大致我

认为可以分为以下两部(其中的细节问题后续展开):

1. 编写驱动的入口和出口函数,此函数会在驱动模块加载和卸载时调用

2. 编写具体的read、write、open等函数,在用户程序使用对应的函数时,该

函数可以被调用。(非必须)

我们先看看一个简单的驱动程序的框架:

以上的代码基本是关于字符型设备驱动的框架结构了。可以看到以上的代码其实就

是一个简单的驱动程序框架了,其实如果没有first_drv_open和first_dev_write两

个函数也是可以的,在硬件上可以正确的安装该驱动,在安装驱动的时候会调用注

册在module_init中的函数,在卸载程序时会调用module_exit中所注册的函数。

但是file_operations结构体依然还是需要定义的,但实际的驱动程序需要操作实际

的硬件,一般都会有open、read、write这类函数。但在此仅仅是为了说明驱动的

最小框架而已。

那么驱动程序写完了,我们来使用测试程序调用一下,以下是测试程序

#include #include #include #include

h> int main(int argc, char **argv) { int fd; //声明设备描述符 int val = 1; //随

便定义变量传入到 fd = open("/dev/xxx", O_RDWR); //根据设备描述符打开设

备 if(fd < 0) //打开失败 printf("can't openn"); write(fd, &val, 4); /

/根据文件描述符调用write return 0; }

Makefile

#驱动程序实际属于内核的一部分,那么在编译的时候就需要使用已经编译好的内

核,来编译驱动程序了,这点尤为重要。 KERN_DIR=/code/LinuxDev/Lab/Kernel

OfLinux/linux-2.6.22.6 #内核目录 all: make -C $(KERN_DIR) M=`pwd` modu

les #M=`pwd`表示,生成的目标放在pwd命令的目录下

# -C代表使用目录中的Makefile来进行编译 clean: make -C $(KERN_DIR)

M=`pwd` modules clean rm -f obj-m += first.o #加载到module

的编译链中,内核会编译生成出来ko文件,作为一个模块

使用Makefile编译驱动程序

编译测试程序

完成了测试程序和驱动程序的编译,那么接下来就是将写好的驱动程序安装在开发

板上 在开发板上使用lsmod命令查看已安装的模块

PS:我的开发板使用的是NFS系统,这个NFS系统是linux服务器所提供的,所

以在Linux服务器上编译完成后就直接切换在了开发板上操作,如果你的开发板使

用的不是NFS系统,那么,还需要把编译出来的测试程序的可执行文件和.ko模块

文件拷贝到开发板的文件系统中,才能执行后续的操作。

lsmod查看系统中已经安装的模块

目前在系统中还没有添加任何的模块。 使用insmod 模块名来加载我们刚才写好的

驱动程序,添加的驱动程序模块是.ko文件

在系统装装在模块

现在可以看到,lsmod以后可以看到已经安装好的驱动程序了,并且在insmod的

时候,调用驱动程序里面我们写好的入口程序first_drv_init函数中的

printk("initn")函数的打印结果。

因此我们知道了在装载驱动程序的时候就会调用驱动程序对应的入口函数。

这时候迫不及待的试一下测试程序,看看能不能正确的open和read吧。

使用测试程序

驱动程序既然已经安装好了,为什么打开测试程序的时候却没法正确的打开呢,回

看我们之前的代码,也没发现错误。

如果我们查看/proc/devices文件,我们会发现,有一个主设备号为55的字符设

备。

这时候如果查看/dev/目录下我们会发现我们写的设备并没有添加在其中。那么我

们为开发板新增加一个设备文件。

添加一个设备文件,然后执行测试程序

添加了设备文件后,在执行测试程序,发现正确的open了,并且调用了write函

数,正确打印了。

mknod命令,第一个参数是设备文件的名字,这个名字要和测试程序中

的打开的相一致 第二参数c代表的是字符设备 55代表的是主设备号 0代

表的是次设备号

驱动程序测试通过了,当我们不需要驱动程序的时候,我们应该将他卸载掉

rmmod 驱动名

写在驱动程序

在我们卸载驱动程序的时候,可以看到调用了驱动程序的出口函数,打印出来了

exit。此时在查看/proc/devices没有设备了。而在/dev/目录下的设备节点则需要

手动来删除。

以上就是一个简单的字符设备驱动程序的框架,驱动程序的在insmod的时候调用

了入口函数,在rmmod的时候调用了出口函数,而当我们调用write或者open

的时候,会调用到驱动程序中在file_operatios结构体中注册的对应的write和

open函数。 如果观察刚才的执行过程,会发现几个问题问题

1. 装载了驱动程序以后,在/proc/devices中设备,分配设备号,但设备号是

在驱动程序中写死的,那么如果设备号被占用,肯定会装载失败;

2. 装载完成了驱动程序以后,实际上还不能直接用测试程序打开对应的设备文

件,因为设备文件并没有自动创建,需要我们手动创建设备节点,这时候才

能使用测试程序来通过打开文件的方式操作驱动程序所对应的硬件。

以上的问题,肯定是有办法解决的,不然我们每次设备都需要这样操作实在也不方

便。那么我们就来改进一下我们的代码来实现自动分配设备号以及创建设备文件吧。

首先关于第一个问题的解决方案很简单,注册驱动程序的时候,如果传入的major

为0,那么系统将会自动为这个驱动程序分配主设备号,同时这个程序也会返回所

分配的主设备号。 第二个问题,解决起来也不是很困难,在Linux中提供了一种

机制是udev,可以用于自动的创建设备,在嵌入式Linux的文件系统,比如

busybox,也有一套简化版的机制,是mdev,在配置文件系统的时候会进行相应

的配置,写完了关于文件系统的文章,我会将链接贴上来。

我们如果在调用驱动程序的入口函数的时候,就使用mdev来创建这个设备文件其

实就行了。使用mdev的时候,需要用到两个结构体,一个是class和class_device。

这两个结构体,都定义在%kernel%/include/linux/device.h的头文件中

为了代码篇幅,这个驱动程序中,就没有定义相关的open、write等函数,那么同

样也就不需要在定义相关的测试函数了。

执行结果

从以上的运行结果我们看出,自动在/proc/devices中,创建了一个主设备号为

252的设备,名字为third_driver,而实际代码中并没有指定对应的主设备号,那

么也就是说明该设备号是由系统自动创建的。 同时我们如果查看一下/dev/目录中,

我们发现在该目录下,创建了一个主设备号为252的设备文件。那么如果用测试

程序来操作,就只需要操作该设备文件就能够操作对应的硬件设备了。

卸载驱动程序

我们卸载了驱动程序后,自然会调用出口函数,我们在出口函数中写了卸载设备文

件的代码,我们发现之前的自动创建的设备文件,也被自动卸载了,这样就解决了

我们之前提出的两个问题。

现在基本完成了设备的框架搭建,并且可以自动生成设备文件了。但是实际现在还

存在两个问题: 1.一直只使用主设备号了,但是次设备号一直没有使用过。其实他

可以帮我们来确认具体我们操作的是哪个具体的硬件设备,那次设备号应该如何使

用呢?

2. 驱动程序是需要来操作硬件的,我们现在还并没有和硬件打交道,那么应该

如何来操作硬件呢?

关于这两个问题,就请看下一篇文章吧,让我们通过一个实验解决我们的问题。