Linux驱动程序设计之入门篇

(转载请注明出处)


一. 明晰基础概念和工作方式

在说驱动之前要先明确一个概念,大家应该都知道,在linux中把一切东西都当作文件。

同样的,一个硬件也被当成一个文件,这些代表硬件的文件我们把它们叫做设备文件

设备文件被统一的放在/dev路径下面。
(用过mount命令的应该很清楚,mount就是将/dev下面的设备文件给挂载到我们的文件系统上)

写过程序的一般都很清楚,当我们打开普通文件之后可以读写,可以对文件作很多事情。

而现在我们面对的是一个设备文件,它代表的是一个硬件,那么读写之类各种操作是由什么东西来完成的呢?

答案是:驱动程序


其实驱动程序在做什么事情呢?答案很简单,就是实现c语言中那些对文件的操作函数。当用户程序调用这些操作函数如open(),read(),write()等等普通的glibc中的函数时,会自动将操作映射到驱动程序中对这些操作的实现。以此来实现对硬件的读写操作。

用户态程序对设备文件的操作的例子就像这样:

int fd;
char buff[10];
fd = open(“/dev/port”, O_RDWR);
write(fd, buff, 1);
close(fd);

大概代码就像这样。

二. 核心数据结构

现在要介绍一个在理解驱动程序设计架构时最重要的数据结构 – file_opeartions
(是我自己以为的,不同意见可以说…嘿嘿)

我不直接说这个数据结构,我用一段最简单的驱动程序的代码来说明。

int xxx_open(struct inode *inode, struct file *filp)
{
    //…
}

int xxx_release(struct inode *inode, struct file *filp)
{
    //…
}

ssize_t xxx_read(struct file *filp, char *buf, size_t count, loff_                   *f_pos)
{
    //…
}

struct file_operations xxx_fops =
{
    .owner = THIS_MODULE,
    .open = xxx_open,
    .release = xxx_release,
    .read = xxx_read;
};

一般聪明的人看了这个代码就会知道这个file_operations是个什么东西。

它定义了由驱动程序实现的一系列具体操作的函数指针。

我们写驱动程序就是去填这一个个的函数,并且把file_operations中的函数指针指向我们自己写的这些函数(我们把这种函数叫做回调函数(callback function))。

当然,file_operations中有很多很多的函数指针.

它的定义在linux的源代码的/include/linux/fs.h文件中,希望读者去看看,而且由于linux每一个版本间的变化,这个数据结构并不是不变的,很可能某一个函数指针在某一个版本就没有掉了。所以在实现自己的驱动程序之前,最好先去查看下这个代码。

三. 设备的注册与注销

代码很简单我写出来大家一看就知道

int xxx_init(void)
{
    register_chrdev(181, “temp_chr”, &xxx_fops);
    //…
}

void xxx_exit(void)
{
    unregister_chrdev(181, “temp_chr”);
    //…
}

moudle_init(xxx_init);
module_exit(xxx_exit);
//注,把上面的代码和这个代码合在一起就是一个完整的linux驱动程序。

register_chrdev这个函数的三个参数分别是:
1.指定的主设备号
2.我给设备的命名
3.file_operations结构

四. 如何编译驱动

Makefile文件就一行:
obj-m := temp_chr.o

编译命令make -C /usr/src/linux-2.6.31/ M=/temp_chr/ modules
(当然,你可以把命令直接写到Makefile文件中去,这样直接make一下就好了)

解释一下这个编译的参数.

第一个路径/usr/src/linux-2.6.31/这个是指定的linux源码存储的位置。
编译出来的驱动程序也只能适用于这个版本的linux内核。如果你要编译适用于其他版本内核的驱动程序,那么就先去下载一个那个版本的内核代码,然后把这个参数改成那个源码的路径就好了。

M=/temp_chr/这个路径是你的驱动程序的存储位置。
如果你就在该目录下面可以改成M=$(pwd)

编译好之后就会出现一个temp_chr.ko文件,这个就是我们编译好的驱动程序。

如果你的这个模块(驱动程序是以内核模块(kernel modules)存在于linux中的,那个.ko文件就是一个内核模块)包括了多个.c文件,那么Makefile文件就这样写:

obj-m := modulename.o
module-objs := file1.o file2.o

五. 如何加载驱动

命令很简单:insmod temp_chr.ko

由于我这个驱动用的是比较老的注册方式,所以不会自动创建设备文件,所以需要手动创建一个:

cd /dev
mknod temp_chr c 181 0

这样就好了,参数的意义
1.temp_chr是被创建设备文件的名字
2.c表示这个是字符设备
3.181是主设备号
4.0是次设备号

这里需要明晰一个概念:设备号

设备号由两个部分构成,主设备号次设备号

主设备号:主设备号标识了设备的类型,拥有相同主设备号的设备使用的驱动程序必定是相同的。(可以容易推出主设备号与驱动程序是一一对应的)

次设备号:次设备号区分了同一类型设备的不同实体,比如一台电脑可能有两块硬盘,一块硬盘的次设备号是1,而另一块硬盘的次设备号就绝对不是1。

数值积分

数值积分方法,对复化simpson和Romberg哪个更好的问题,我也不知道怎么比较
Romberg更耗内存,但是它可以对精度控制的相当好…
这个优点简直就是让我没有办法不选它…
复化simpson相当的直接,就相当于把被积区间给分成n份,然后对每份都用simpson公式…
如果是分成n份的话,就需要调用2n + 2次func…所以对算法的复杂度可以控制的很好…
不过,浙大的算法库上用的是Romberg…


//这只是一个函数的示例,使用时按照需求自行修改(增加系数个数,修改函数内容等等)
double func(double x){
return log(x + cos(x));
}

//最简单的simpson积分,积分端点a, b
//只是随便写写,基本没有什么很好的精度

double Simpson(double a, double b){
return (b - a) * (func(a) + func(b) + 4 * func((a+b)/2)) / 6.0;
}

//复化simpson积分,积分端点a, b,以及你想把区间分成的份数n
//这个在于积分区间不变或者变数不大的情况下特别好用
//如果一定要用,则要随着积分区间的的增大,按比例调整n的大小

double C_simpson(double a, double b, int n){
double h = (b - a) / (2 * n), s1 = 0, s2 = 0, x;
for(int i = 1; i < n; i++){
x = a + 2 * i * h;
s1 += func(x);
}
for(int i = 0; i < n; i++){
x = a + (2 * i + 1) * h;
s2 += func(x);
}
return h * (func(a) + 4 * s2 + 2 * s1 + func(b))/3;
}
//Romberg数值积分,输入积分端点a, b,精度要求deta
//计算次数相对较多,效率并不很高,占用内存也相对较大
//优点是可以控制精度
//当数组t的大小不够算出所需的精度的时候(此时函数会返回100000),可以把t开大

double Romberg(double a, double b, double deta){
double h = b - a, t[10][10]; //数组t的大小可以按照所需的精度自行控制
memset(t, 0, sizeof(t));
unsigned int m = 1;;
t[0][0] = h * (func(a) + func(b))/2;
for(int k = 1; k < 10;k++){
h /= 2;
for(int i = 0; i < m; i++)
t[k][0] += func(a + (2 * i + 1) * h);
t[k][0] = 0.5 * (t[k - 1][0] + t[k][0] * h * 2);
for(int i = 1; i <= k; i++){
t[k][i] = t[k][i - 1] + (t[k][i - 1] - t[k - 1][i - 1]) /(pow(2.0, 2.0 * i) - 1);
if(fabs(t[k][i] - t[k - 1][i - 1]) < deta) return t[k][i];
}
m <<= 1;
}
return 100000;
}

Newton插值法

Newton插值法的基函数非常的容易记:

Nn(x) = A0 + A1(x – x0) + A2(x – x0)(x – x1) + … + An(x – x0)…(x – xn-1)

系数Ai也非常容易求,是一个递推的过程:

A0 = [x0]f (x = x0处的零阶差商)
A1 = [x0,x1]f (x = x0,x1处的一阶差商)
A2 = [x0,x1,x2]f (x = x0,x1,x2处的二阶差商)
…………………………
An = [x0,x1,x2,…,xn]f (x =x0,x1,x2,…,xn处的n阶差商)

其中差商的递推关系是:

[xi]f = f(xi)

[x0,x1,x2,…,xk]f = ([x1,x2,…,xk]f – [x0,x1,x2,…,xk-1]f)/(xk – x0)

所以,整个过程就很轻易咯。。

另:当每个插值点是等距的时候,过程将大大简化。。。

放代码:

//一共有n个点,数组x,y存放函数表格,aim为插值点
//d[i]存放第i阶差商(i = 0, 1, 2,..., n-1)
double Newton(int n, double* x, double* y, double aim){
double tnew, *d = new double[n];
for(int i = 0; i < n; i++)
d[i] = *(y + i);
for(int i = 1; i < n; i++)
for(int j = n - 1; j >= i; j--)
d[j] = (d[j] - d[j-1])/(*(x + j) - *(x + j - i));
tnew = d[n - 1];
for(int i = n - 2; i >= 0; i--)
tnew = d[i] + tnew * (aim - *(x+i));
return tnew;
}

Lagrange插值法

把一些以前写的杂七杂八的东西给贴上来,准备维护一下这个blog

由于Lagrange插值法给出了一组非常好的基函数(即Lagrange插值基函数)

所以,我们可以不需要任何的计算(因为系数就是f(xi))就可以直接得出Lagrange插值的目标函数
给出C语言代码:

//一共有n个已知点,数组x,y存放函数表格,aim为插值点
//Pai,Tai分别表示li(x)的分子和分母的连乘积,用Slag存放计算结果
double Lagrange(int n, double *x, double *y, double aim){
double Pai = 1, Slag = 0, Tai = 1;
for(int i = 0; i < n; i++)
Pai *= (aim - *(x + i));
for(int j = 0; j < n; j++){
Tai = 1;
for(int i = 0; i < n; i++)
if(i != j) Tai *= (*(x + j) - *(x + i));
Slag += (*(y + j) * Pai / ((aim - *(x + j)) * Tai));
}
return Slag;
}