linux driver文件夹,Linux驱动程序模块调试方法

第6章编写Linux驱动程序

6.1  Linux驱动程序概述

LINUX中的驱动设计是嵌入式LINUX开发中十分重要的部分,它要求开发者不仅要熟悉LINUX的内核机制、驱动程序与用户级应用程序的接口关系、考虑系统中对设备的并发操作等等,而且还要非常熟悉所开发硬件的工作原理。这对驱动开发者提出了比较高的要求,本章是给大家了解驱动设计提供一个简单入门的一个实例,并不需要提供太多与硬件相关的内容,这部分应该是通过仔细阅读芯片厂家提供的资料来解决。

驱动程序的作用是应用程序与硬件之间的一个中间软件层,驱动程序应该为应用程序展现硬件的所有功能,不应该强加其他的约束,对于硬件使用的权限和限制应该由应用程序层控制。但是有时驱动程序的设计是跟所开发的项目相关的,这时就可能在驱动层加入一些与应用相关的设计考虑,主要是因为在驱动层的效率比应用层高,同时为了项目的需要可能只强化或优化硬件的某个功能,而弱化或关闭其他一些功能;到底需要展现硬件的哪些功能全都由开发者根据需要而定。驱动程序有时会被多个进程同时使用,这时我们要考虑如何处理并发的问题,就需要调用一些内核的函数使用互斥量和锁等机制。

驱动程序主要需要考虑下面三个方面:提供尽量多的选项给用户,提高驱动程序的速度和效率,尽量使驱动程序简单,使之易于维护。

LINUX的驱动开发调试有两种方法,一种是直接编译到内核,再运行新的内核来测试;二是编译为模块的形式,单独加载运行调试。第一种方法效率较低,但在某些场合是唯一的方法。模块方式调试效率很高,它使用insmod工具将编译的模块直接插入内核,如果出现故障,可以使用rmmod从内核中卸载模块。不需要重新启动内核,这使驱动调试效率大大提高。

模块中必须的两个基本函数:在Linux 2.4内核中是函数init_module和cleanup_module;在Linux 2.6的内核中是宏module_init(your_init_func)和module_exit(your_exit_func)。初始化函数的作用一般是分配资源、注册设备方法等,退出函数的作用一般是释放所申请的资源等。

11411615_4.jpg

6.1.1驱动程序与应用程序的区别

应用程序一般有一个main函数,从头到尾执行一个任务;驱动程序却不同,它没有main函数,通过使用宏module_init(初始化函数名);将初始化函数加入内核全局初始化函数列表中,在内核初始化时执行驱动的初始化函数,从而完成驱动的初始化和注册,之后驱动便停止等待被应用软件调用。驱动程序中有一个宏moudule_exit(退出处理函数名)注册退出处理函数。它在驱动退出时被调用。

应用程序可以和GLIBC库连接,因此可以包含标准的头文件,比如、,在驱动程序中是不能使用标准C库的,因此不能调用所有的C库函数,比如输出打印函数只能使用内核的printk函数,包含的头文件只能是内核的头文件,比如。

6.1.2内核版本与编译器的版本依赖

当模块与内核链接时,insmod会检查模块和当前内核版本是否匹配,每个模块都定义了版本符号__module_kernel_version,这个符号位于模块文件的ELF头的.modinfo段中。只要在模块中包含,编译器就会自动定义这个符号。

每个内核版本都需要特定版本的编译器的支持,高版本的编译器并不适合低版本的内核,比如UP-NETARM3000实验仪中的LINUX-2.4.17-uc1的内核需要2.95.3的GCC版本编译器。Linux-2.4版本的Insmod命令装载模块时,首先从/lib/modules目录和内核相关的子目录中查找模块文件,如果需要从当前目录装载,使用insmod  ./module.o。

6.1.3主设备号和次设备号

传统方式中的设备管理中,除了设备类型外,内核还需要一对称作主次设备号的参数,才能唯一标识一个设备。主设备号相同的设备使用相同的驱动程序,次设备号用于区分具体设备的实例。比如PC机中的IDE设备,一般主设备号使用3,Windows下进行的分区,一般将主分区的次设备号为1,扩展分区的次设备号为2、3、4,逻辑分区使用5、6….。

设备操作宏MAJOR()和MINOR()可分别用于获取主次设备号,宏MKDEV()用于将主设备号和次设备号合并为设备号,这些宏定义在include/linux/kdev_t.h中。对于LINUX中对设备号的分配原则可以参考Documentation/devices.txt。

对于查看/dev目录下的设备的主次设备号可以使用如下命令:

[/mnt/yaffs]ls  /dev -l

crw-------    1 root     root       5,   1 Jan  1 00:00 console

crw-------    1 root     root       5,  64 Jan  1 00:00 cua0

crw-------    1 root     root       5,  65 Jan  1 00:00 cua1

crw-rw-rw-    1 root     root       1,   7 Jan  1 00:00 full

drwxr-xr-x    1 root     root            0 Jan  1 00:00 keyboard

crw-r-----    1 root     root       1,   2 Jan  1 00:00 kmem

crw-r-----    1 root     root       1,   1 Jan  1 00:00 mem

drwxr-xr-x    1 root     root            0 Jan  1 00:00 mtd

drwxr-xr-x    1 root     root            0 Jan  1 00:00 mtdblock

crw-rw-rw-    1 root     root       1,   3 Jan  1 00:00 null

crw-r-----    1 root     root       1,   4 Jan  1 00:00 port

crw-------    1 root     root     108,   0 Jan  1 00:00 ppp

crw-rw-rw-    1 root     root       5,   2 Jan  1 00:00 ptmx

crw-r--r--    1 root     root       1,   8 Jan  1 00:00 random

6.1.4设备文件

设备类型、主次设备号是内核与设备驱动程序通信时所使用的,但是对于开发应用程序的用户来说比较难于理解和记忆,所以LINUX使用了设备文件的概念来统一对设备的访问接口,在引入设备文件系统(devfs)之前LINXU将设备文件放在/dev目录下,设备的命名一般为设备文件名+数字或字母表示的子类,例如/dev/hda1、/dev/hda2等。

在LINUX-2.4内核中引入了设备文件系统(devfs),所有的设备文件作为一个可以挂装的文件系统,这样就可以被文件系统进行统一管理,从而设备文件就可以挂装到任何需要的地方。命名规则也发生了变化,一般将主设备建立一个目录,再将具体的子设备文件建立在此目录下。比如在UP-NETARM3000中的MTD设备为:/dev/mtdblock/0。

6.1.5设备驱动程序接口

通常所说的设备驱动程序接口是指结构file_operations{},它定义在include/linux/fs.h中。

file_operations数据结构说明

struct file_operations {

struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

int (*readdir) (struct file *, void *, filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, struct dentry *, int datasync);

int (*fasync) (int, struct file *, int);

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

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);

#ifdef MAGIC_ROM_PTR

int (*romptr) (struct file *, struct vm_area_struct *);

#endif /* MAGIC_ROM_PTR */

};

file_operations结构是整个LINUX内核的重要数据结构,它也是file{}、inode{}结构的重要成员,下面分别说明结构中主要的成员:

Owner    module的拥有者。

Llseek重新定位读写位置。

Read从设备中读取数据。

Write向字符设备中写入数据。

Readdir只用于文件系统,对设备无用。

Ioctl控制设备,除读写操作外的其他控制命令。

Mmap将设备内存映射到进程地址空间,通常只用于块设备。

Open打开设备并初始化设备。

Flush清除内容,一般只用于网络文件系统中。

Release关闭设备并释放资源。

Fsync实现内存与设备的同步,如将内存数据写入硬盘。

Fasync实现内存与设备之间的异步通讯。

Lock文件锁定,用于文件共享时的互斥访问。

Readv在进行读操作前要验证地址是否可读。

Writev在进行写操作前要验证地址是否可写。

在嵌入式系统的开发中,我们一般仅仅实现其中几个接口函数:read、write、ioctl、open、release,就可以完成应用系统需要的功能。

file数据结构说明

struct file {

struct list_head    f_list;

struct dentry        *f_dentry;

struct vfsmount  *f_vfsmnt;

struct file_operations  *f_op;

atomic_t               f_count;

unsigned int               f_flags;

mode_t                f_mode;

loff_t                    f_pos;

unsigned long     f_reada, f_ramax, f_raend, f_ralen, f_rawin;

struct fown_struct       f_owner;

unsigned int         f_uid, f_gid;

int                        f_error;

unsigned long      f_version;

/* needed for tty driver, and maybe others */

void               *private_data;

/* preallocated helper kiobuf to speedup O_DIRECT */

struct kiobuf         *f_iobuf;

long                            f_iobuf_lock;

};

file结构中与驱动相关的重要成员说明

我们将struct file结构指针定义为flip,以便于下面说明。

f_mode标识文件的读写权限

f_pos当前读写位置,类型为loff_t是64位的数,只能读不能写

f_flag文件标志,主要用于进行阻塞/非阻塞型操作时检查

f_op文件操作的结构指针,内核在OPEN操作时对此指针赋值。

private_data Open系统调用在调用驱动程序的open方法前,将此指针值NULL,驱动程序可以将这个字段用于任何目的,一般用它指向已经分配的数据,但在内核销毁file结构前要在release方法中释放内存。

f_dentry文件对应的目录项结构,一般在驱动中用filp->f_dentry->d_inode访问索引节点时用到它。

6.1.6驱动接口的实现过程

我们先看看实验代码框架

#define DEVICE_NAME             "demo"

static ssize_t  demo_write(struct file *filp,const char * buffer, size_t count)

{   char drv_buf[];

copy_from_user(drv_buf , buffer, count);

}

static ssize_t  demo_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)

{

char drv_buf[];

copy_to_user(buffer, drv_buf,count);

….

}

static int demo_ioctl(struct inode *inode, struct file *file,

unsigned int cmd, unsigned long arg)

{

}

static int demo_open(struct inode *inode, struct file *file)

{

}

static int  demo_release(struct inode *inode, struct file *filp)

{

MOD_DEC_USE_COUNT;

DPRINTK("device release\n");

return 0;

}

static struct file_operations demo_fops = {

owner:      THIS_MODULE,

write:         demo_write,

read:         demo_read,

ioctl: demo_ioctl,

open:        demo_open,

release:    demo_release,

};

#ifdef CONFIG_DEVFS_FS

static devfs_handle_t  devfs_demo_dir, devfs_demoraw;

#endif

static int __init demo_init(void)

{

int  result;

#ifdef CONFIG_DEVFS_FS

devfs_demo_dir = devfs_mk_dir(NULL, "demo", NULL);

devfs_demoraw = devfs_register(devfs_demo_dir, "0", DEVFS_FL_DEFAULT,

demo_Major, demo_MINOR, S_IFCHR | S_IRUSR | S_IWUSR,

&demo_fops, NULL);

#else

SET_MODULE_OWNER(&demo_fops);

result = register_chrdev(demo_Major, "scullc", &demo_fops);

if (result < 0) return result;

if (demo_Major == 0) demo_Major = result; /* dynamic */

#endif

printk(DEVICE_NAME " initialized\n");

return 0;

}

static void __exit  demo_exit(void)

{

unregister_chrdev(demo_major, "demo");

kfree(demo_devices);

printk(DEVICE_NAME " unloaded\n");

}

module_init(demo_init);

module_exit(demo_exit);

其中的加深部分代码: static struct file_operations demo_fops = {…}完成了将驱动函数映射为标准接口,devfs_register()和register_chrdev()函数完成将驱动向内核注册。

static struct file_operations demo_fops = {

owner:      THIS_MODULE,

write:         demo_write,

read:         demo_read,

ioctl: demo_ioctl,

open:        demo_open,

release:    demo_release,

};

上面的这种特殊表示方法不是标准C的语法,这是GNU编译器的一种特殊扩展,它使用名字对进行结构字段的初始化,它的好处体现在结构清晰,易于理解,并且避免了结构发生变化带来的许多问题。目前,更多的是使用如下的表示方法。

static struct file_operations demo_fops = {

.owner =THIS_MODULE,

.write =demo_write,

.read  =demo_read,

.ioctl =demo_ioctl,

.open  =demo_open,

.release=demo_release,

};

Open方法

Open方法提供给驱动程序初始化设备的能力,从而为以后的设备操作做好准备,此外open操作一般还会递增使用计数,用以防止文件关闭前模块被卸载出内核。在大多数驱动程序中Open方法应完成如下工作:

1.递增使用计数

2.检查特定设备错误。

3.如果设备是首次打开,则对其进行初始化。

4.识别次设备号,如有必要修改f_op指针。

5.分配并填写filp->private_data中的数据。

Release方法

与open方法相反,release方法应完成如下功能:

1.释放由open分配的filp->private_data中的所有内容

2.在最后一次关闭操作时关闭设备

3.使用计数减一

read和write方法

ssize_t  demo_write(struct file *filp,const char * buffer, size_t count,loff_t *ppos)

ssize_t  demo_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)

read方法完成将数据从内核拷贝到应用程序空间,write方法相反,将数据从应用程序空间拷贝到内核。对于者两个方法,参数filp是文件指针,count是请求传输数据的长度,buffer是用户空间的数据缓冲区,ppos是文件中进行操作的偏移量,类型为64位数。由于用户空间和内核空间的内存映射方式完全不同,所以不能使用象memcpy之类的函数,必须使用如下函数:

unsigned long copy_to_user  (void *to,const void *from,unsigned long count);

unsigned long copy_from_user(void *to,const void *from,unsigned long count);

read的返回值

1.返回值等于传递给read系统调用的count参数,表明请求的数据传输成功。

2.返回值大于0,但小于传递给read系统调用的count参数,表明部分数据传输成功,根据设备的不同,导致这个问题的原因也不同,一般采取再次读取的方法。

3.返回值=0,表示到达文件的末尾。

4.返回值为负数,表示出现错误,并且指明是何种错误。

5.在阻塞型io中,read调用会出现阻塞。

Write的返回值

1.返回值等于传递给write系统调用的count参数,表明请求的数据传输成功。

2.返回值大于0,但小于传递给write系统调用的count参数,表明部分数据传输成功,根据设备的不同,导致这个问题的原因也不同,一般采取再次读取的方法。

3.返回值=0,表示没有写入任何数据。标准库在调用write时,出现这种情况会重复调用write。

4.返回值为负数,表示出现错误,并且指明是何种错误。错误号的定义参见

5.在阻塞型io中,write调用会出现阻塞。

Ioctl方法

Ioctl方法主要用于对设备进行读写之外的其他控制,比如配置设备、进入或退出某种操作模式,这些操作一般都无法通过read/write文件操作来完成,比如在UP-NETARM3000中的SPI设备通道的选择操作,无法通过write操作控制,这就是ioctl操作的功能。

用户空间的ioctl函数的原型为:

int ioctl(inf fd,int cmd,…)

其中的…代表可变数目的参数表,实际中是一个可选参数,一般定义为:

int ioctl(inf fd,int cmd,char *argp)

驱动程序中定义的ioctl方法原型为:

int (*ioctl) (struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)

inode和filp两个指针对应应用程序传递的文件描述符fd,cmd不会被修改地传递给驱动程序,可选的参数arg则无论用户应用程序使用的是指针还是其他类型值,都以unsigned long的形式传递给驱动。

Ioctl方法的命令编号确定

由于为了防止向不该控制的设备发出正确的命令,LINUX驱动的ioctl方法中的cmd参数推荐使用唯一编号,编号方法并根据如下规则定义:

编号分为4个字段:

1.    type(类型):也称为幻数,8位宽。

2.    number(号码):顺序数,8位宽。

3.    direction(方向):如果该命令有数据传输,就要定义传输方向,2位宽,可使用的数值:

a)    _IOC_NONE

b)    _IOC_READ

c)    _IOC_WRITE

4.    size(大小):数据大小,宽度与体系结构有关,在ARM上为14位。

这些定义在中可以找到。其中还定义了一些用于构造命令号的宏。内核目前没有使用ioctl的cmd参数,所以你如果自己简单定义一个如1、2、3这样的命令号也是可以的。

Ioctl方法的返回值

Ioctl通常实现一个基于switch语句的各个命令的处理,对于用户程序传递了不合适的命名参数时,POSIX标准规定应返回-ENOTTY,返回-EINVAL是以前常见的方法。

不能使用与LINUX预定义命令相同的号码,因为这些命令号码会被内核sys_ioctl函数识别,并且不再将命令传递给驱动的ioctl。Linux针对所有文件的预定义命令的幻数为“T”。所以我们不应使用TYPE为”T”的幻数。

devfs_register函数

其原型为:

devfs_register(devfs_handle_t dir, const char *name, unsigned int flags,

unsigned int major, unsigned int minor,

umode_t mode, void *ops, void *info)

其中的参数说明如下:

Dir新创建的设备文件的父目录,一般设为null,表示父目录为/dev

Name设备名称,如想包含子目录,可以直接在名字中包含’/’

Flags     Devfs标志的位掩码。

Major主设备号如果在flags参数中指定为DEVFS_FL_AUTO_DEVNUM,则主次设备号就无用了。

Minor次设备号,

Mode设备的访问模式

Ops设备的文件操作数据结构指针

Info filp->private_data的默认值。

6.1.7关于阻塞型IO

read调用有时会出现当前没有数据可读,但是马上就会有数据到达,这时就会使用睡眠并等待数据的方法,这就是阻塞型IO,write也是同样的道理。在阻塞型IO中涉及到如何使进程睡眠、如何唤醒,如何在阻塞的情况查看是否有数据。

睡眠与唤醒

当进程等待一个事件时,应该进入睡眠,等待被事件唤醒,这主要是由等待队列这种机制来处理多个进程的睡眠与唤醒。这里要使用到如下几个函数和结构:

这个结构和函数的定义在文件中。

wait_queue_head_t

struct __wait_queue_head {

wq_lock_t lock;

struct list_head task_list;

#if WAITQUEUE_DEBUG

long __magic;

long __creator;

#endif

};

typedef struct __wait_queue_head  wait_queue_head_t;

初始化函数

static inline void init_waitqueue_head(wait_queue_head_t *q)

如果声明了等待队列,并完成初始化,进程就可以睡眠。根据睡眠的深浅不同,可调用sleep_on的不同变体函数完成睡眠。

一般会用到如下几个函数:

sleep_on(wait_queue_head_t *queue);

interruptible_sleep_on(wait_queue_head_t *queue);

sleep_on_timeout(wait_queue_head_t *queue, long timeout);

interruptible_sleep_on_timeout(wait_queue_head_t *queue, long timeout);

wait_event(wait_queue_head_t queue,int condition);

wait_event_ interruptible (wait_queue_head_t queue,int condition);

我们大多数情况下应使用“可中断”的函数,也就是带interruptible的函数。还要注意,睡眠进程被唤醒并不一定代表有数据,也有可能被其他信号唤醒,所以醒来后需要测试condition。

6.1.8并发访问与数据保护

1.可以使用循环缓冲区并且避免使用共享变量

这种方法是类似于“生产者消费者问题”的处理方法,生产者向缓冲区写入数据,消费者从缓冲区读取数据。

2.使用自旋锁实现互斥访问

自旋锁的操作函数定义在文件中。其中包含了许多宏定义,主要的函数如下:

spin_lock_init(lock))初始化锁

spin_lock(lock)获取给定的自旋锁

spin_is_locked(lock)查询自旋锁的状态

spin_unlock_wait(lock)       )释放自旋锁

spin_unlock(lock)释放自旋锁

spin_lock_irqsave(lock, flags)保存中断状态获取自旋锁

spin_lock_irq(lock)不保存中断状态获取自旋锁

spin_lock_bh(lock)获取给定的自旋锁并阻止底半部的执行

LINXU中还提供了称为读者/写者自旋锁,这种锁的类型为rwlock_t,可以通过文件查看更详细的内容。

6.1.9中断处理

中断是所有现在微处理器的重要功能,LINUX驱动程序中对于中断的处理方法一般使用以下几个函数:

请求安装某个中断号的处理程序:

extern int request_irq(unsigned int irq,

void (*handler)(int, void *, struct pt_regs *),

unsigned long flag,

const char * dev_name,

void *dev_id);

释放中断

extern void free_irq(unsigned int, void *);

request_irq函数中的参数说明如下:

irq:请求的中断号

void (*handler)(int, void *, struct pt_regs *),要安装的处理函数指针

unsigned long flag,与中断管理相关的位掩码

const char * dev_name,用于在/proc/interrupts中显示的中断的拥有者

void *dev_id);用于标识产生中断的设备号。

其中的flag中的可以设置的位定义如下:

SA_INTERRUPT是快速中断程序,一般运行在中断禁用状态

SA_SHIRQ中断可以在设备之间共享

SA_SAMPLE_RANDOM指出产生的中断对/dev/random和/dev/urandom设备使用的商池有贡献。从这些设备读取会返回真正的随机数。

一般我们应该在设备第一次open时使用request_irq函数,在设备最后一次关闭时使用free_irq。

编写中断处理函数的注意事项:

中断处理程序与普通C代码没有太大不同,不同的是中断处理程序在中断期间运行,它有如下限制:

1.不能向用户空间发送或接受数据,

2.不能执行有睡眠操作的函数

3.不能调用调度函数

6.1.10内核源码目录分布

arch

与体系结构相关的代码全部放在这里,我们的实验设备中使用的是其中的armnommu和arm目录。

Drivers

此目录包括所有的驱动程序,下面又建立了多个目录,分别存放各个分类的驱动程序源代码。drivers目录是内核中最大的源代码存放处,大约占整个内核的一多半。其中我们经常会用到的目录有:

drivers/char

字符设备是drivers目录中最为常用,也许是最为重要的目录,因为其中包含了大量与驱动程序无关的代码。通用的tty层在这里实现,console.c定义了linux终端类型,vt.c中定义了虚拟控制台;lp.c中实现了一个通用的并口打印机的驱动,并保持设备无关性;kerboard.c实现高级键盘处理,它导出handle_scancode函数,以便于其他与平台相关的键盘驱动使用。我们的大部分实验也是放在这个目录下。

Driver/block

其中存放所有的块设备驱动程序,也保存了一些设备无关的代码。目录中最重要的文件是ll_rw_blk.c,它是一个底层块读写文件,blkpg.c实现了块设备的分区和几何参数的通用处理,它导出的公共函数为blk_ioctl,可以被其他块设备驱动程序使用。rd.c实现了RAM磁盘,nbd.c实现了网络块设备,loop.c实现了回环块设备。

Drives/ide

专门存放针对IDE设备的驱动。

Drivers/scsi

存放SCSI设备的驱动程序,当前的cd刻录机、扫描仪、U盘等设备都依赖这个SCSI的通用设备。

Drivers/net

存放网络接口适配器的驱动程序,还包括一些线路规程的实现,但不实现实际的通信协议,这部分在顶层目录的net目录中实现。

Drivers/video

这里保存了所有的帧缓冲区视频设备的驱动程序,整个目录实现了一个单独的字符设备驱动。/dev/fb设备的入口点在fbmem.c文件中,该文件注册主设备号并维护一个此设备的清单,其中记录了哪一个帧缓冲区设备负责哪个次设备号。

Drivers/media

这里存放的代码主要是针对无线电和视频输入设备,比如目前流行的usb摄像头。

fs

此目录下包括了大量的文件系统的源代码,其中在嵌入式开发中主要使用的包括:

devfs、cramfs、ext2、,jffs2、romfs、yaffs、vfat、nfs、proc等。

文件系统是LINUX中非常重要的子系统,这里实现了许多重要的系统调用,比如exec.c文件中实现了execve系统调用;用于文件访问的系统调用在open.c、read_write.c等文件中定义,select.c实现了select和poll系统调用,pipe.c和fifo.c实现了管道和命名管道,mkdir、rmdir、rename、link、symlink、mknod等系统调用在namei.c中实现。

文件系统的挂装和卸载和用于临时根文件系统的initrd在super.c中实现。Devices.c中实现了字符设备和块设备驱动程序的注册函数;file.c、inode.c实现了管理文件和索引节点内部数据结构的组织。Ioctl.c实现ioctl系统调用。

include:

这里是内核的所有头文件存放的地方,其中的linux目录是头文件最多的地方,也是驱动程序经常要包含的目录。

init:

linux的main.c程序,通过这个比较简单的程序,我们可以理解LINUX的启动流程。

6.2驱动的调试

6.2.使用printk函数

最简单的方法是使用printk函数,printk函数中可以使用附加不同的日志级别或消息优先级,如下例子:

printk(KERN_DEBUG “Here is :%s: %i \n”,__FILE,__LINE__);

上述例子中宏KERN_DEBUG和后面的“之间没有逗号,因为宏实际是字符串,在编译时会由编译器将它和后面的文本拼接在一起。在头文件中定义了8种可用的日志级别字符串:

#define    KERN_EMERG    "<0>"    /* system is unusable           */

#define    KERN_ALERT    "<1>"   /* action must be taken immediately*/

#define    KERN_CRIT    "<2>"    /* critical conditions    */

#define    KERN_ERR    "<3>"    /* error conditions            */

#define    KERN_WARNING    "<4>"    /* warning conditions   */

#define    KERN_NOTICE    "<5>"    /* normal but significant condition */

#define    KERN_INFO    "<6>"    /* informational            */

#define    KERN_DEBUG    "<7>"    /* debug-level messages   */

未指定优先级的默认级别定义在/kernel/printk.c中:

#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */

当优先级的值小于console_loglevel这个整数变量的值,信息才能显示出来。而console_loglevel的初始值DEFAULT_CONSOLE_LOGLEVEL也定义在/kernel/printk.c中。如果系统运行了klogd和syslogd则内核将把消息输出到/var/log/messages中。

#define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */

通过对/proc/sys/kernel/printk的访问来改变console_loglevel的值:

#echo 1 > /proc/sys/kernel/printk

#cat /proc/sys/kernel/printk

1       4       1       7

四个数字的含义:当前的loglevel、默认loglevel、最小允许的loglevel、引导时的默认loglevel。

6.2.2使用/proc文件系统

/proc文件系统是由程序创建的文件系统,内核利用它向外输出信息。/proc目录下的每一个文件都被绑定到一个内核函数,这个函数在此文件被读取时,动态地生成文件的内容。典型的例子就是ps、top命令就是通过读取/proc下的文件来获取他们需要的信息。

大多数情况下proc目录下的文件是只读的。使用/proc的模块必须包含头文件。

接口函数read_proc可用与输出信息,其定义如下:

int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);

其中的参数说明如下:

参数说明

Page将要写入数据的缓冲区指针。

Start数据将要写入的页面位置。

Offset页面中的偏移量。

count写入的字节数。

eof指向一个整形数,当没有更多数据时,必须设置这个参数.

data驱动程序特定的数据指针,可用于内部使用。

函数的返回值表示实际放入页面缓冲区的数据字节数。

如何建立函数与/proc目录下的文件之间的关联

使用create_proc_read_entry()函数,其定义如下:

struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,

struct proc_dir_entry *parent);

其中的参数含义说明如下:

Name文件名称

Mode文件权限

Parent文件的父目录的指针,为null时代表父目录为/proc

6.2.3使用ioctl方法

ioctl系统调用会调用驱动的ioctl方法,我们可以通过设置不同的命名号来编写一些测试函数,使用ioctl系统调用在用户级调用这些函数进行调试。

6.2.4使用strace命令进行调试

strace命令是一个功能强大的工具,它可以显示用户空间的程序发出的全部系统调用,不仅可以显示调用,还可以显示调用的参数和用符号方式表示的返回值。

Strace有几个有用的参数:

-t显示调用发生的时间

-T显示调用花费的时间

-e限定被跟踪的系统调用的类型

-o将输出重定向到一个文件

Strace是从内核接收信息,所以它可以跟踪没有使用调试方式编译的程序。还可以跟踪一个正在运行的进程。可以使用它生成跟踪报告,交给应用程序开发人员;但是对于内核开发人员同样有用。我们可以通过每次对驱动调用的输入输出数据的检查,来发现驱动的工作是否正常。

6.3模块方式加载驱动程序

参考驱动代码demo.c如下,其中的demo_read、demo_write函数完成驱动的读写接口功能,do_write函数实现将用户写入的数据逆序排列,通过读取函数读取转换后的数据。这里只是演示接口的实现过程和内核驱动对用户的数据的处理。Demo_ioctl函数演示ioctl调用接口的实现过程。

/**************************************************************

demo.c  2.4

***************************************************************/

//#define CONFIG_DEVFS_FS

#ifndef __KERNEL__

#  define __KERNEL__

#endif

#ifndef MODULE

#  define MODULE

#endif

#include

#include

#include

#include

#include    /* printk() */

#include    /* kmalloc() */

#include        /* everything... */

#include     /* error codes */

#include     /* size_t */

#include

#include     /* O_ACCMODE */

#include     /* COPY_TO_USER */

#include      /* cli(), *_flags */

#define DEVICE_NAME             "demo"

#define demo_MAJOR 254

#define demo_MINOR 0

static int MAX_BUF_LEN=1024;

static char drv_buf[1024];

static int WRI_LENGTH=0;

/*************************************************************************************/

/*逆序排列缓冲区数据*/

static void do_write()

{

int i;

int len = WRI_LENGTH;

char tmp;

for(i = 0; i < (len>>1); i++,len--){

tmp = drv_buf[len-1];

drv_buf[len-1] = drv_buf[i];

drv_buf[i] = tmp;

}

}

/*************************************************************************************/

static ssize_t  demo_write(struct file *filp,const char *buffer, size_t count)

{

if(count > MAX_BUF_LEN)count = MAX_BUF_LEN;

copy_from_user(drv_buf , buffer, count);

WRI_LENGTH = count;

printk("user write data to driver\n");

do_write();

return count;

}

/*************************************************************************************/

static ssize_t  demo_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)

{

if(count > MAX_BUF_LEN)

count=MAX_BUF_LEN;

copy_to_user(buffer, drv_buf,count);

printk("user read data from driver\n");

return count;

}

/*************************************************************************************/

static int demo_ioctl(struct inode *inode, struct file *file,

unsigned int cmd, unsigned long arg)

{

printk("ioctl runing\n");

switch(cmd){

case 1:printk("runing command 1 \n");break;

case 2:printk("runing command 2 \n");break;

default:

printk("error cmd number\n");break;

}

return 0;

}

/*************************************************************************************/

static int demo_open(struct inode *inode, struct file *file)

{

sprintf(drv_buf,"device open sucess!\n");

printk("device open sucess!\n");

return 0;

}

/*************************************************************************************/

static int  demo_release(struct inode *inode, struct file *filp)

{

MOD_DEC_USE_COUNT;

printk("device release\n");

return 0;

}

/*************************************************************************************/

static struct file_operations demo_fops = {

owner:          THIS_MODULE,

write:  demo_write,

read:   demo_read,

ioctl:  demo_ioctl,

open:  demo_open,

release:         demo_release,

};

/*************************************************************************************/

#ifdef CONFIG_DEVFS_FS

static devfs_handle_t  devfs_demo_dir, devfs_demoraw;

#endif

/*************************************************************************************/

static int __init demo_init(void)

{

#ifdef CONFIG_DEVFS_FS

devfs_demo_dir = devfs_mk_dir(NULL, "demo", NULL);

devfs_demoraw = devfs_register(devfs_demo_dir, "0", DEVFS_FL_DEFAULT,

demo_MAJOR, demo_MINOR, S_IFCHR | S_IRUSR | S_IWUSR,

&demo_fops, NULL);

#else

int  result;

SET_MODULE_OWNER(&demo_fops);

result = register_chrdev(demo_MAJOR, "demo", &demo_fops);

if (result < 0) return result;

//    if (demo_MAJOR == 0) demo_MAJOR = result; /* dynamic */

#endif

printk(DEVICE_NAME " initialized\n");

return 0;

}

/*************************************************************************************/

static void __exit  demo_exit(void)

{

unregister_chrdev(demo_MAJOR, "demo");

//kfree(demo_devices);

printk(DEVICE_NAME " unloaded\n");

}

/*************************************************************************************/

module_init(demo_init);

module_exit(demo_exit);

insmod demo.o   //加载驱动

rmmod demo     //卸载驱动

lsmod           //查看驱动装载情况

编写用户级测试程序,参考代码test.c如下,使用命令:

gcc  test.c  –o  test

直接编译即可。

#include

#include

#include

#include

#include

void showbuf(char *buf);

int MAX_LEN=32;

int main()

{

int fd;

int i;

char buf[255];

for(i=0; i

buf[i]=i;

}

fd=open("/dev/demo",O_RDWR);

if(fd < 0){

printf("####DEMO  device open fail####\n");

return (-1);

}

printf("write %d bytes data to /dev/demo \n",MAX_LEN);

showbuf(buf);

write(fd,buf,MAX_LEN);

printf("Read %d bytes data from /dev/demo \n",MAX_LEN);

read(fd,buf,MAX_LEN);

showbuf(buf);

ioctl(fd,1,NULL);

ioctl(fd,4,NULL);

close(fd);

return 0;

}

void showbuf(char *buf)

{

int i,j=0;

for(i=0;i

if(i%4 ==0)

printf("\n%4d: ",j++);

printf("%4d ",buf[i]);

}

printf("\n*****************************************************\n");

}

6.4内核方式加载驱动程序

静态加载(内核方式加载)就是把驱动程序直接编译到内核里,系统启动后可以直接调用。静态加载的缺点是调试起来比较麻烦,每次修改一个地方都要重新编译下载内核,效率较低。动态加载利用了LINUX的module特性,可以在系统启动后用insmod命令把驱动程序(.ko文件)添加上去,在不需要的时候用rmmod命令来卸载。在台式机上一般采用动态加载的方式。在嵌入式产品里可以先用动态加载的方式来调试,调试完毕后再编译到内核里。

某个功能或者设备驱动可以直接build-in内核,也可以作为内核模块,在要用时再调入。2.4内核和2.6内核是当前用的两大系列内核版本,区别很大,两者方法不同,下面将分别说明。

将demo驱动demo.c代码拷贝到$KERNEL/drivers/char/下。

编辑$KERNEL/drivers/char目录下面的Kconfig文件,加入新的键盘配置选项 ,例如:

添加

config DEMO

tristate " DEMO support"

default y

help

The " DEMO " is a simple driver, Y for build in ,M for Module.

配置解释:

configDEMO

上面的config是配置关键字,DEMO表示新配置选项的标识符

tristate "DEMOsupport"

中tristate表示是可以配置成Y,M,N三中情况

default y

配置默认是什么选项

help

The " DEMO " is a simple driver, Y for build in ,M for Module.

配置的帮助

修改Makefile编译文件

编辑$KERNEL/drivers/char目录下面的Makefile文件,加入新的键盘编译选项,例如

obj-$(CONFIG_DEMO)               += demo.o

注意:Kconfig中的配置标识符要和编译选项中红色标识符一致,编译的目标demo.o名称要和源代码的demo.c名称一致,这是系统强行规定的。

make menuconfig

文本菜单配置方式配置内核选项

导入源代码预配置的文件

配置新加入的驱动

新的配置选项在上图中显示出来了,可以配置成y(build in),m(module),n(不编译),默认是y,把它配置成M(module),然后退出,保存配置

make zImage

编译内核,生成内核映像文件

make modules

编译内核模块

在目录arch/arm/boot下面可以看到新生成的zImage内核映像文件

在目录drivers/char/下面可以看到demo的内核模块demo.ko

驱动程序文件名为demo.c,其源码如下:

/**************************************************************

demo.c 2.6

***************************************************************/

#include

#include

#include

#include    /* printk() */

#include    /* kmalloc() */

#include        /* everything... */

#include     /* error codes */

#include     /* size_t */

#include

#include     /* O_ACCMODE */

#include     /* COPY_TO_USER */

#include      /* cli(), *_flags */

#include

#define CDRIVER_NAME  "demo"

int CDRIVER_MAJOR=0;

int CDRIVER_MINOR=0;

static int MAX_BUF_LEN=1024;

static char drv_buf[1024];

static int WRI_LENGTH=0;

int dev_count=1;

dev_t demo_dev;

struct cdev *demo_cdev;

/*************************************************************************************/

static void do_write(void)

{

int i;

int len = WRI_LENGTH;

char tmp;

for(i = 0; i < (len>>1); i++,len--){

tmp = drv_buf[len-1];

drv_buf[len-1] = drv_buf[i];

drv_buf[i] = tmp;

}

}

/*************************************************************************************/

static ssize_t  demo_write(struct file *filp,const char *buffer, size_t count)

{

if(count > MAX_BUF_LEN)count = MAX_BUF_LEN;

copy_from_user(drv_buf , buffer, count);

WRI_LENGTH = count;

printk(KERN_INFO"user write data to driver\n");

do_write();

return count;

}

/*************************************************************************************/

static ssize_t  demo_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)

{

if(count > MAX_BUF_LEN)

count=MAX_BUF_LEN;

copy_to_user(buffer, drv_buf,count);

printk("user read data from driver\n");

return count;

}

/*************************************************************************************/

static int demo_ioctl(struct inode *inode, struct file *file,

unsigned int cmd, unsigned long arg)

{

switch(cmd){

case 1:printk("runing command 1 \n");break;

case 2:printk("runing command 2 \n");break;

default:

printk("error cmd number\n");break;

}

return 0;

}

/*************************************************************************************/

static int demo_open(struct inode *inode, struct file *file)

{

try_module_get(THIS_MODULE);

//MOD_INC_USE_COUNT;

sprintf(drv_buf,"device open sucess!\n");

printk("device open sucess!\n");

return 0;

}

/*************************************************************************************/

static int  demo_release(struct inode *inode, struct file *filp)

{

//MOD_DEC_USE_COUNT;

module_put(THIS_MODULE);

printk("device release\n");

return 0;

}

/*************************************************************************************/

static struct file_operations demo_fops = {

.owner =THIS_MODULE,

.write =demo_write,

.read  =demo_read,

.ioctl =demo_ioctl,

.open  =demo_open,

.release=demo_release,

};

/*************************************************************************************/

/*************************************************************************************/

static int __init demo_init(void)

{

int result;

struct class *demo_class;

if(CDRIVER_MAJOR){

demo_dev=MKDEV(CDRIVER_MAJOR,CDRIVER_MINOR);

result=register_chrdev_region(demo_dev,dev_count,CDRIVER_NAME);

}

else

{

result=alloc_chrdev_region(&demo_dev,CDRIVER_MINOR,dev_count,CDRIVER_NAME);

CDRIVER_MAJOR=MAJOR(demo_dev);

}

if(result<0)

{

printk(KERN_ERR"Can not get major %d\n",CDRIVER_MAJOR);

return -1;

}

demo_cdev=cdev_alloc();

if(demo_cdev!=NULL)

{

cdev_init(demo_cdev,&demo_fops);

demo_cdev->ops=&demo_fops;

demo_cdev->owner=THIS_MODULE;

if(cdev_add(demo_cdev,demo_dev,dev_count))

printk(KERN_NOTICE"Something is wrong adding demo_cdev!\n");

else

printk("Success adding demo_cdev!\n");

}

else

{

printk(KERN_ERR"Register demo_dev error!\n");

return -1;

}

demo_class=class_create(THIS_MODULE,"demo");

class_device_create(demo_class,NULL,MKDEV(CDRIVER_MAJOR,0),NULL,"demo");

return 0;

}

/*************************************************************************************/

static void __exit  demo_exit(void)

{

cdev_del(demo_cdev);

unregister_chrdev_region(demo_dev,dev_count);

printk(" demo unloaded\n");

}

/*************************************************************************************/

module_init(demo_init);

module_exit(demo_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("xuyuanchao @ict.ac.cn");

MODULE_DESCRIPTION("DEMO DRIVER!");

当然在编写2.6内核驱动程序之前应该已经自己建立好一个2.6的内核源码树(我这里是基于s3c2410移植的源码树,本处该源码是放在宿主机的/home/src/linux-2.6.16目录下的),如果没有的话,那么需要自己去建立好这个源码树.自己编写的模块化驱动程序可以不放在内核源码之内,但是此外还需要一个自己编写一个Makefile文件。

在宿主机的终端下,进入驱动程序目录内,敲入命令:

#make

就会在该目录下生成demo.ko文件,这就是2.6内核下生成的驱动加载模块,注意是.ko文件,不同于2.4内核下的.o文件.

把该demo.ko文件拷贝到目标板上,在minicom终端下进入该文件目录,敲入:

#insmod demo.ko

如果终端显示有demoinitialized则表示加载成功.

这时可以用命令lsmod查看动态加载模块:

#lsmod

当然,可以用如下命令查看devfs文件系统信息:

#cat /proc/devices

如果用卸载该模块,敲入命令:

#rmmod demo

加载驱动程序后,可以编写一个简单的测试程序,并交叉编译

#arm-linux-gcc test.c -o test

将二进制文件同样拷贝到目标板上,运行:

#./test

即可看到实验效果。

6.5内核方式加载驱动的demo示例

第一步:将demo.c驱动程序加到内核中,并重新生成zImage映像文件

程序目录:

11411615_1.jpg

其中:demo.c是驱动程序(内核级),test_demo.c是测试程序(用户级)

(1)将demo.c拷贝到/uclinux/uClinux-2.4.x/drivers/char下(一般来说字符设备的驱动都放于此文件目录下):

(2)打开demo.c,将下面粗体部分注释掉,否则编译时可能会出错:

/*

//#define CONFIG_DEVFS_FS

#ifndef __KERNEL__

#  define __KERNEL__

#endif

#ifndef MODULE

#  define MODULE

#endif

*/

(3)修改/uclinux/uClinux-2.4.x/drivers/char下的Config.in文件,加入一行下面粗体部分(不要在if中加),该文件是在做内核裁减的时候使用的配置清单

#############################################################################

#

# uClinux options

#

bool 'Demo driver support' CONFIG_DEMO

if [ "$CONFIG_M68328" = "y" ]; then

bool '68328 serial support' CONFIG_68328_SERIAL

if [ "$CONFIG_68328_SERIAL" = "y" ]; then

bool 'Support RTS/CTS on 68328 serial support' CONFIG_68328_SERIAL_RTS_CTS

fi

if [ "$CONFIG_PILOT" = "y" ]; then

bool '68328 digitizer support' CONFIG_68328_DIGI

fi

fi

(4)修改/uclinux/uClinux-2.4.x/drivers/char下的Makefile文件,在合适的地方加入下面语句(注:不要加到if….endif中),该文件是在重新编译内核使用。

obj-$(CONFIG_DEMO) += demo.o

CONFIG_DEMO与Config.in中定义的CONFIG_DEMO对应上,demo.o是demo.c编译之后的目标文件。

(5)切换到/uclinux/uClinux-2.4.x/下,输入make menuconfig,对内核进行裁减

11411615_2.jpg

选择字符设备,当进入下面的界面时,敲空格键选中第一个demo driver support。

11411615_3.jpg

然后选中exit退出,当询问是否保存配置文件时,选择”yes”。

(6)保存退出后,输入make dep,生成依赖关系,第一次重新编译内核时必须要完成此步。

注:

make menuconfig

make dep

make zImage

等命令一定要在/uclinux/uClinux-2.4.x下输入,在其他目录下输入将提示失败。

(7)然后输入make zImage,完成裁减后的内核的编译打包,打完的包zImage位于/uclinux/uClinux-2.4.x/arch/armnommu/boot/zImage,可用以下方法将其放在根目录下:

命令如下:

cp arch/armnommu/boot/zImage  /

注意:上述过程可能会报错,导致编译无法通过,根据错误提示,修改dma.c文件,将出错的350-355行注释掉,然后重新编译。具体位置为:

/uclinux/uClinux-2.4.x/arch/armnommu/mach-s3c44b0/

第二步:重 新 烧 写 内 核

准备工作:保证串口线接好,并且blob以及文件系统ramdisk已经存在,否则需要重新烧写。

如果在linux环境下,可按照讲义或pdf上的详细步骤进行如下操作。

(1)打开minicom,重新启动开发板,进入到blob>提示下。

(2)blob>xdownload kernel,然后根据提示找到zImage,然后download即可。

(3)blob>flash kernel,完成烧写。

如果在windows下,方法同以前的演示。

第三步:用test_demo.c程序测试驱动是否正常工作

准备工作:配置IP,NFS服务,关闭防火墙。NFS服务的配置需要注意,如果linux系统不支持图形化配置,可以直接修改/etc目录下的exports文件

/共享目录IP(rw,sync)

通常我们写为: /uclinux 192.168.99.*(rw,sync) #,然后保存退出。

使用showmount –e localhost进行测试,如果能看到/uclinux等信息,说明nfs服务端配置成功。

在开发板上进行mount操作时,建议退到根目录下,否则有时不成功:

[/]mount –t nfs 192.168.99.154:/uclinux  /mnt/yaffs

[/]cd  /mnt/yaffs

[/mnt/yaffs]ls

如果能列出主机/uclinux下的所有文件或目录则说明mount正确,否则请检查你的开发板上的IP地址设置是否符合同一网段的规范,是否连接网线。

(1)修改/uclinux/exp/kernel/driver下的Makefile文件如下,注意粗体字部分

#TOPDIR  := $(shell cd ..; pwd)

TOPDIR  := .

KERNELDIR = /uclinux/uClinux-2.4.x

#说明kernel的路径

#KERNELDIR = /opt/host/armv4l/src/linux

#KERNELDIR = /data/arm2410/kernel

INCLUDEDIR = $(KERNELDIR)/include

CROSS_COMPILE=arm-uclibc-

#使用arm-uclibc-gcc做编译器

#TARGET = demo.o hello.o test_demo

TARGET = hello.o test_demo

all: $(TARGET)

#demo.o: demo.c

#     $(CC) -c $(CFLAGS) $^ -o $@

test_demo: test_demo.o

$(CC) $^  -static -elf2flt  -o $@

#参考ARM3000-uClinux开发指南.pdf文档上的说明,将可执行文件改为扁平格式的文件

(2)修改test_demo.c应用程序:

将其中的

fd=open("/dev/demo",O_RDWR);

改为:

fd=open("/dev/demo/0",O_RDWR);

(3)在/uclinux/exp/kernel/driver下执行make clean然后make,生成可执行文件test_demo。

(4)修改权限chmod +x test_demo。

(5)在minicom中通过nfs找到test_demo文件存放目录,然后执行。

6.6 ads7843与spi驱动源代码注释

分析驱动整体框架,画出流程图,给出每一行注释。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值