第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.
配置解释:
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映像文件
程序目录:
其中: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驱动源代码注释
分析驱动整体框架,画出流程图,给出每一行注释。