南京11月车展时间表:Linux驱动程序模块调试方法

来源:百度文库 编辑:偶看新闻 时间:2024/04/28 15:37:33

第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)。初始化函数的作用一般是分配资源、注册设备方法等,退出函数的作用一般是释放所申请的资源等。

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.

配置解释:

config DEMO

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

tristate " DEMO support" 

中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映像文件

程序目录:

其中: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,对内核进行裁减

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

 

然后选中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驱动源代码注释

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