【嵌入式LINUX操作系统】字符设备驱动程序编写举例
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
《嵌入式Linux应用开发菜鸟进阶》
第11章
对字符设备驱动的模块框架有一个宏观的认识:
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
︙
#include<XXX>
这里根据实际的驱动需要添加头文件
static int mem_major = 251;
︙
/*这里定义驱动需要的一些静态数据或者指针,当然作为全局变量,一般不要轻易使用这些静态变量,它们很占内存,并且浪费资源*/
︙
实现file_operation中挂接的函数
static const struct file_operations mem_operation={
.owner = THIS_MODULE,
︙
};
根据驱动需要实现相应的系统调用函数
static int mymem_init(void)
1
{
︙
}
模块驱动的注册函数
static void mymem_exit(void)
{
︙
}
模块的释放函数
MODULE_AUTHOR("Lin Hui");
MODULE_LICENSE("GPL");
定义模块编写的作者以及遵循的协议
module_init(mymem_init);
module_exit(mymem_exit);
定义模块初始化入口函数
以上就是一个驱动基本不变的部分,
针对字符变化的部分进行详细的讲解。
首先是字符设备的注册。
字符设备的注册主要分为4 步:设备号、分配设备号、定义并初始化file_operation结构体和字符设备的注册。
其中,设备号与分配设备号在11.1节中已经详述,这里不再重复。
下面介绍字符设备注册的详细步骤。
(1)设备号。
(2)分配设备号。
(3)定义并初始化file_operations结构体。
file_operations结构体用于连接设备号和驱动程序的操作。
在该结构体内部包含一组函数
2
指针,这些函数用来实现系统调用。
通常情况下,要注册如下几个函数。
(1)struct module *owner:用来指向拥有该结构体的模块。
(2)ssize_t read(struct file *filp,char __user *buf,size_t count,loff_t *f_ops):用来从设备中读取数据。
其中,
❑filp为文件属性结构体指针。
❑buf为用户态函数使用的字符内存缓冲。
❑count为要读取的数据数。
❑f_ops为文件指针的偏移量。
❑ssize_t write(struct file *filp,const char __user *buf,size_t count,loff_t *f_ops):用来向设备输入数据。
(3)int open(struct inode *inode,struct file *):该函数用来打开一个设备文件。
(4)int release(struct inode *inode,struct file *):该函数用来关闭一个设备文件。
该结构体的初始化形式如下例:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.read = read,
.write = write,
.open = open,
.release = release,
}
内核内部使用struct cdev结构来表示字符设备。
在进行内核调用设备的操作之前,必须分配或注册一个或者多个该结构体。
该结构体包含在头文件中,一般步骤如下。
首先定义该结构体:
3
struct cdev my_cdev;
然后即可初始化该结构体,使用如下函数进行初始化:
int cdev_init(struct cdev*dev,struct file_operations*fops).
再后定义该结构体中的一个所有者字段:
my_cdev.owner = THIS_MODULE;
最后向模块添加该结构体:
int cdev_add(struct cdev*dev,dev_t dev_num,usigned int count);
其中,dev是cdev结构体,dev_num是该设备对应的第一个设备编号,count则是与该设备关联的设备编号数量。
以上就是一个字符设备驱动需要做的事情,当然在字符设备的注册过程中,可能还会涉及一些内核数据的处理,比如分配字符设备驱动需要的空间,以及初始化一些内核协议等工作。
因此针对不同驱动数据的处理要添加不同的Linux内核技术,这才是字符设备驱动开发的核心,这些内容都会在后续章节中通过实际的例子进行分析和学习。
11.2.2 字符设备的释放
对于字符设备驱动的释放,需要做的工作就是对注册时或者数据处理时,申请到的字符设备驱动的核心数据予以释放。
我们知道Linux内核的资源相当珍贵,所以不用的时候就要释放出来,其中包括注销设备号、移除字符设备、释放申请到的Linux内核空间、释放申请到的Linux内核子系统相关的数据结构等。
一般的字符设备驱动的释放包含以下两方面内容。
(1)移除字符设备函数:
void cdev_del(struct cdev *dev);
4
(2)注销设备号函数:
unregister_chrdev_region(dev_t first,unsigned int count);
以上两个函数一般用在模块出口函数中。
在介绍字符设备驱动的注册前我们就已经学习了字符设备驱动模型中的编程架构,下面将针对这些内容进行详细的解说。
1.确定一些版本信息和建立内核头文件
#include <linux/modules.h>
#include <linux/version.h>
MODULE_AUTHOR("Lin Hui");
MODULE_LICENSE("GPL");
上面添加的是Linux内核驱动必需的一些头文件、编写驱动模块用到的协议的定义以及模块开发者的说明,这些是模型中的一部分。
2.建立系统调用与驱动程序之间的关联
将系统调用和驱动程序关联起来需要一个非常关键的数据结构—struct file_operations。
file_operations结构中每一个成员的名字都对应着一个系统调用。
用户进程利用系统调用在对设备文件进行读写等操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该内核函数。
这是Linux 的设备驱动程序工作的基本原理。
驱动开发的主要工作就是编写这些系统调用子函数,并填充进file_operations数据结构的各个域,也就是编写XXX_read、XXX_write等函数和构建file_operations结构体。
5
3.驱动程序的编写(mymem.c)
❑包含基本的头文件和驱动需要的头文件。
❑编写基本功能函数,比如XXX_read()、XXX_write()、XXX_joctl()等。
这些函数被调用时系统进入核心态。
❑定义struct file_operations结构的对象,填充结构体。
结构体中功能的顺序不能改变,若一些功能没有实现就用NULL填充,已经实现的功能如read()、write()分别被添加到对应的位置。
这一步实现的是函数的注册。
到这里驱动程序的主体就写好了,现在需要把驱动程序嵌入内核。
❑注册设备驱动程序,使用register_chrdev_region或者alloc_chrdev_region函数注册字符型设备。
dev_t devno = MKDEV(mem_major,0);
if(mem_major)
result = register_chrdev_region(devno,1,"mymem");
else{
result = alloc_chrdev_region(&devno,0,1,"mymem");
mem_major = MAJOR(devno);
}
此时已经完成了注册设备驱动程序的大部分内容。
当然,如果需要实现混杂设备驱动程序的编写以及自创建设备文件,则需要实现这些内容。
在用rmmod卸载模块时,mymem_exit函数被调用,它释放字符设备mymem在系统字符设备表中占有的表项。
static void mymem_exit(void)
{
6
cdev_del(&cdev);
unregister_chrdev_region(MKDEV(mem_major,0),1);
}
到这里,mymem.c基本就编写完了。
可以说,一个简单的字符设备就写好了。
4.编译
到目前为止已经编写好了驱动程序,下面就要编写makefile文件了。
makefile文件用于编译我们所写的驱动模块代码编译文件,这个文件的格式必须要掌握,如果读者还没有掌握,那么复习一下基础知识,再接着看下面的代码:
ifneq ($(KERNELRELEASE),)
obj-m :=mymem.o
else
KDIR :=/home/linhui/kernel/linux-2.6.29
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-linux-
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers modul* *~
endif
上面的makefile文件是编译Linux内核代码的一个样例,其中需要改变的只有KDIR中的内核路径代码,还有就是我们使用的具体代码运行的架构以及编译工具,在这里选择arm 和arm-linux-。
当然,要注意Linux内核代码路径必须经过预先的编译才能成功地编译我们的代码。
也就是说,必须先编译一次内核代码并使用同样的代码架构和编译工具。
至此makefile已经编写完成,只需要执行make命令就可以完成驱动程序的编译了。
7
$make
驱动程序已经编译完成,现在把它安装到系统中。
$ insmod -f mymem.o
安装成功后在/proc/devices文件中就可以看到设备test,并可以看到主设备号。
如果要卸载,则运行如下命令:rmmod mymem
5.创建设备节点
上一章中已经介绍了如何手动地创建设备节点以及自创建设备文件的知识,这时就可以派上用场了。
手动创建方式如下:
$ mkmod /dev/mymem c major minor
c指字符设备,major是主设备号,minor是从设备号,一般可以设置为0。
如果是自动创建设备驱动,则不需要做这一步。
至于详细的自动创建设备文件的内容,在前面几章中曾提到,在接下来的实际字符设备驱动的开发中也会用到。
第12章
我们开发一个基于内存的字符设备驱动。
该字符设备驱动的开发基于上一章用到的字符设备驱动的开发流程来实现。
在这个驱动中,需要在内存中分配一块4MB大小的内存空间,在驱动中提供对该内存的读写、控制和定位方法,用户空间的进程能够通过Linux相应的系统调用访问这片内存以及操作。
在这一章中,我们将学习到如下知识:
1.简单字符设备驱动mymem的数据结构的填充。
2.简单字符设备驱动mymem的注册与释放。
8
3.简单字符设备驱动mymem的打开和关闭以及llseek函数的实现。
4.简单字符设备驱动mymem的应用程序的编写和测试。
12.1 简单字符驱动的数据结构
在开发一个简单的字符设备驱动之前,先要设计好整个驱动涉及的数据结构。
这非常关键,因为我们开发出来的驱动其实是对底层数据进行处理,考虑到字符设备驱动是基于内存的模拟硬件,因此设计一个合理的保存字符设备内容的结构非常关键。
12.1.1 定义字符设备驱动的设备数据结构
这一章需要开发的是一个基于内存的简单字符驱动,所以驱动结构内容需要包含内存的首地址以及需要开辟出来的内存大小。
我们需要定义一个关于字符设备的头文件,这个文件用来保存该字符设备的设备信息。
在这里,我们的设备是基于一块4MB的内存,可以实现数据的读、写和定位3个简单的功能,因此定义一个mem_dev的设备结构体来保存内存的数据首地址和数据区的大小。
考虑到Linux内核的内存空间异常珍贵,所以内存空间不能分配过多,否则如果分配的内存大于1GB,那么程序和内核有可能出现崩溃现象。
鉴于此我们只分配4MB的空间用于实验。
创建一个头文件mymem.h,mymem.h中的实现内容如下:
#ifndef _MYMEM_H_
#define _MYMEM_H_
#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 251
#endif
#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
9
#endif
define the struct of mem chardev.
struct mem_dev{
char*data; poiter to the memory
unsigned long size; define the size of the memory
};
#endif
头文件中的内容还包含了默认的主设备号251、需要分配内存的大小以及一个mem_dev 的数据结构,其中的mem_dev结构体保存的是分配内存的首地址以及分配到的大小。
在以后的驱动编写中要养成一个习惯,就是编写一个驱动代码的头文件,将驱动中用到的一些常量、设备驱动用到的字符设备驱动数据结构保存在这个头文件中。
这样无论是从驱动的阅读上还是驱动结构上,都可以使驱动数据与驱动代码分开,以便于在驱动测试或者驱动修改时快速定位数据或者代码的位置,从而有利于驱动的开发。
同时这也是一名优秀的驱动开发者必备的一项技能。
12.1.2 定义file_operation结构和挂接相应的系统调用函数
在定义了字符设备的数据结构和头文件之后,需要确定字符设备驱动的功能和需要实现的函数操作,也就是上层应用需要的系统调用。
我们设计的字符设备驱动需要实现字符设备的打开,并可以对内存空间的数据进行读、写、内存定位以及内存空间的释放等功能,因此需要实现open、read、write、llseek和release 5个功能函数。
file_operation结构体如下。
static const struct file_operations mem_operation={
.owner = THIS_MODULE,
.read = mem_read,
.write = mem_write,
10
.llseek = mem_llseek,
.open = mem_open,
.release = mem_release,
};
这里有一个技巧,就是当读驱动比较多时,只要看一下这个file_operation的结构,就会发现这个驱动所要完成的功能了。
当然,这只是针对功能相对简单的字符设备驱动程序,sysfs文件系统、I2C等设备就没有这么简单了。
12.2 简单字符驱动设计
在定义好需要的驱动设备的数据结构后,就要开始设计驱动程序了。
在编写驱动程序之前,需要有一个清晰的字符设备驱动编程框架,也就是在没填充驱动实现内容之前需要的一个代码架构,具体如下。
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include<linux/device.h> device_create()funtion use!
11
#include"mymem.h"
static int mem_major = 251;
struct mem_dev *mem_devp; dev struct poiter
struct cdev cdev;
static dev_t devno;
module_param(mem_major,int ,S_IRUGO);
open the memory device
int mem_open(struct inode *inode,struct file *filp)
{
}
int mem_release(struct inode * inode,struct file * filp)
{
}
realize the open funtion
static ssize_t mem_read(struct file *filp,char __user *buf,size_t size,loff_t *ppos)
{
}
static loff_t mem_llseek(struct file *filp,loff_t offset,int whence)
{
}
static const struct file_operations mem_operation={
.owner = THIS_MODULE,
.read = mem_read,
.write = mem_write,
12
.llseek = mem_llseek,
.open = mem_open,
.release = mem_release,
};
/*
static int malloc_device(dev_t *devno)
{
}
static void mymem_exit(void)
{
}
MODULE_AUTHOR("Lin Hui");
MODULE_LICENSE("GPL");
module_init(mymem_init);
module_exit(mymem_exit);
如上所述,我们得到了一个字符设备驱动的基本编程框架,在这个框架里分别要实现mymem_init和mymem_exit驱动加载和退出函数,mem_read读函数,mem_write写函数,mem_llseek内存定位函数,mem_open字符驱动打开函数,mem_release字符驱动释放函数。
接下来,将分模块来实现以上这些函数。
到这里,读者应该对字符设备驱动编写有一个宏观的了解了。
“骨架”都搭好了,接下来就是“填肉”了。
12.2.1 字符设备驱动的加载与卸载
mymem字符设备驱动的模块加载和卸载与上一章介绍的字符设备的注册与释放区别不大,但是在这里,我们将利用在前面几章中讲到的动态加载设备文件的相关知识,也就是说,不
13
旗开得胜需要自创建设备文件,而是由内核自动完成。
字符设备驱动的加载—mymem_init():
static int mymem_init(void)
{
int result;
struct class * myclass;
apply for dev number
dev_t devno = MKDEV(mem_major,0);
if(mem_major)
result = register_chrdev_region(devno,1,"mymem");
else{
result = alloc_chrdev_region(&devno,0,1,"mymem");
mem_major = MAJOR(devno);
}
if(result<0)
return result;
initialize the cdev struct(char)
cdev_init(&cdev,&mem_operation);
cdev.owner = THIS_MODULE;
cdev.ops = &mem_operation;
register the char device
cdev_add(&cdev,MKDEV(mem_major,0),1);
cdev_add(&cdev,devno,1);
malloc for the mem_dev struct and device
14
result = malloc_device(&devno);
malloc for the mem_dev
mem_devp = kmalloc(sizeof(struct mem_dev),GFP_KERNEL);
if(!mem_devp)
{
result = -ENOMEM;
unregister_chrdev_region(devno,1);
return result;
}
memset(mem_devp,0,sizeof(struct mem_dev));
malloc for dev
mem_devp->size = MEMDEV_SIZE;
mem_devp->data = kmalloc(MEMDEV_SIZE,GFP_KERNEL);
memset(mem_devp->data,0,MEMDEV_SIZE);
sema_init(&mem_devp->sem,1); initialize the semaphore
myclass = class_create(THIS_MODULE,"my_device_driver");
device_create(myclass,NULL,MKDEV(mem_major,0),NULL,"mymem");
return 0;
}
mymem字符设备驱动的卸载—mymem_exit():在字符设备驱动的卸载中,需要将申请的字符设备进行释放,并且释放申请到的内存以及释放申请到的设备号。
static void mymem_exit(void)
{
cdev_del(&cdev);
15
kfree(mem_devp);
unregister_chrdev_region(MKDEV(mem_major,0),1);
unregister_chrdev_region(devno,1);
}
12.2.2 字符设备驱动的打开与关闭
在字符设备的驱动中,特别是在这些较为简单的字符设备中,一般都不会为驱动的open与close函数实现过多的内容。
但是这里有一点需要注意,在驱动开发中,大部分的Linux驱动工程师都会遵循一个“潜规则”,就是将文件的私有数据private_data指向设备结构体,在read()、write()、ioctl()、llseek()等函数中通过private_data访问设备结构体。
这是因为在这些函数的原型参数中没有struct inode*这个结构体。
这样就很容易封装好设备结构与字符设备的内容。
而在字符设备的关闭中则不做任何动作,因为当应用程序关闭打开的驱动文件后,我们的驱动只需要返回一个0表示返回成功即可。
字符设备的打开—mem_open():从inode中获得字符设备驱动的结构数据并保存到file 文件的私有数据中。
open the memory device
int mem_open(struct inode *inode,struct file *filp)
{
struct mem_dev *dev;
gain the minor number
int num = MINOR(inode->i_rdev);
if(num >= 1)
return -ENODEV;
dev = &mem_devp[num];
16
filp-> private_data = dev;
return 0;
}
字符设备的关闭—mem_release():字符设备驱动释放函数不需要实现任何动作,只需要返回0即可。
int mem_release(struct inode * inode,struct file * filp)
{
return 0;
}
12.2.3 字符设备驱动的读写函数
我们设计的是一个基于内存的字符设备,因此驱动中的读写函数是整个驱动的中心。
在读函数中,需要把open中保存在file私有数据区的内容提取出来,也就是字符设备中所保存的内存首地址及字符设备长度。
将保存在内存空间的数据通过copy_to_user()函数复制到用户空间。
具体的驱动读函数mem_read()实现如下:
realize the open funtion
static ssize_t mem_read(struct file *filp,char __user *buf,size_t size,loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
17
int ret = 0;
struct mem_dev *dev = filp->private_data;
if(p >= MEMDEV_SIZE)
return 0;
if(count > MEMDEV_SIZE-p)
count = MEMDEV_SIZE - p;
realize the copy_to_user() funtion
if(copy_to_user(buf,(void*)(dev->data+p),count))
{
ret = -EFAULT;
}else{
*ppos +=count;
ret = count;
printk(KERN_INFO "read %u bytes from %lu(driver print)\n\n",count,p);
}
return ret;
}
和读函数一样,我们需要获得保存在file中的字符设备私有数据的内容,并将用户空间的数据通过copy_from_user()函数复制到内存中保存。
具体的驱动写函数mem_write()实现如下:
static ssize_t mem_write(struct file *filp,const char __user *buf,size_t size,loff_t *ppos)
{
18
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data;
if(p >=MEMDEV_SIZE)
return 0;
if(count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
if(copy_from_user(dev->data + p,buf,count))
ret = -EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "writen %u bytes from %lu(driver print)\n",count, p);
}
return ret;
12.2.4 字符设备驱动的llseek函数
在上层应用中,可能会用到llseek这个系统调用来定位访问文件,所以在底层驱动中需要实现一个操作函数。
其中的定位可以在文件头(SEEK_SET,0)、当前位置(SEEK_CUR,1)、文件尾(SEEK_END,2),而whence就是第3个参数,表示当前位置使用0、1、2三个数字进行区别。
字符设备驱动的mem_llseek()函数实现如下:
19
static loff_t mem_llseek(struct file *filp,loff_t offset,int whence)
{
loff_t newpos;
switch(whence)
{
case 0: SEEK_SET
newpos = offset;break;
case 1: SEEK_CUR
newpos = filp->f_pos + offset; break;
case 2: SEEK_END
newpos = MEMDEV_SIZE;break;
default:
return -EINVAL;
}
if((newpos<0)||(newpos>MEMDEV_SIZE))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
12.3 应用程序测试
12.3.1 应用程序设计原理
至此,我们已经开发出一个简单的字符设备驱动,可以加载到Linux内核中。
但是还没有设计应用程序,所以这一节将开发一个应用程序来测试我们的驱动是否可以使用。
在测试应用
20
程序中需要实现的内容有使用open()函数打开字符设备驱动,使用write()函数向字符设备驱动中写入内容,通过llseek()函数定位内存的位置,并通过read()函数读内存中的内容。
最后将读出来的内容打印出来。
12.3.2 程序代码
下面是应用程序的代码:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
int fp;
int tmp;
char buf[4096];
char mybuf[4096];
strcpy(buf,"This my first char driver!");
printf("BUF(befor write):%s\n",buf);
fp=open("/dev/mymem",O_CREAT|O_RDWR,S_IRUSR|S_IWUSR);
if(fp <= 0)
{
printf("open mymem faile!\n");
return -1;
21
}
printf("open mymem success!\n");
tmp = write(fp,buf,50);
if(tmp <= 0)
{
printf("write faile!\n");
return -1;
}
printf("write ‘ %s ’ to mymem!\n",buf);
lseek(fp,0,SEEK_SET);
strcpy(buf,"buf is NULL!");
printf("buf(after write): %s\n",buf);
tmp = read(fp,buf,50);
if(tmp<=0)
{
printf("read mymem faile!\n");
return -1;
}
printf("buf(after read):%s\n",buf);
return 0;
}
在应用测试程序中,需要充分地调度系统调用函数来测试我们的驱动功能。
我们的设计思路是
(1)先向驱动中写数据,然后打印我们写进去的数据char1。
(2)接着调用llseek函数重新定位操作文件的位置,将位置设为文件头。
22
(3)调用read函数将我们写进去的数据char2读取出来并打印在终端上。
当完成以上3步时,会发现写进去的char1与读取出来的char2的数据相同,这说明驱动已经可以工作了,即驱动程序可以正常运行。
当然,驱动的正确运行并不等于说我们写的驱动没有错误,也许我们的驱动还有bug(缺陷)。
有时候,我们写的程序存在严重的缺陷,这个就有赖于我们对驱动程序的认识和学习,从而不断积累经验。
12.4 简单字符设备驱动运行效果
12.4.1 makefile程序的编写
首先需要编写一个makefile文件来帮助我们编译编写的mymem.c的驱动程序。
makefile 文件的内容如下:
ifneq ($(KERNELRELEASE),)
obj-m := mymem.o
else
KDIR := /home/guoqian/6/motion/src/kernel/linux-2.6.29
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-linux-
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers modul* *~
endif
12.4.2 字符设备驱动运行效果
图12.1和图12.2是编写好的驱动代码、驱动程序应用程序的编译以及运行效果图。
23
图12.1 驱动的编译以及应用测试程序的编译
24
图12.2 在开发板上测试成功的驱动测试图
《从实践中学嵌入式Linux应用程序开发》
7.3 GPIO驱动程序实例
7.3.1 GPIO工作原理
FS2410开发板的S3C2410处理器具有117个多功能通用I/O(GPIO)端口引脚,包括GPIO 8个端口组,分别为GPA(23个输出端口)、GPB(11个输入/输出端口)、GPC(16个输入/输出端口)、GPD(16个输入/输出端口)、GPE(16个输入/输出端口)、GPF(8个输入/输出端口)、GPH(11个输入/输出端口)。
根据各种系统设计的需求,通过软件方法可以将这些端口配置成具有相应功能(例如,外部中断或数据总线)的端口。
为了控制这些端口,S3C2410处理器为每个端口组分别提供几种相应的控制寄存器,其中最常用的有端口配置寄存器(GPACON ~GPHCON)和端口数据寄存器(GPADAT ~GPHDAT)。
因为大部分I/O引脚可以提供多种功能,通过配置寄存器(PnCON)设定每
25
个引脚用于何种目的,数据寄存器的每位将对应于某个引脚上的输入或输出,所以通过对数据寄存器(PnDAT)的位读写,可以进行对每个端口的输入或输出。
在此主要以发光二极管(LED)和蜂鸣器为例,讨论GPIO设备的驱动程序。
它们的硬件驱动电路的原理图如图7.4所示。
图7.4 LED(左)和蜂鸣器(右)的驱动电路原理图
在图7.4中,使用S3C2410处理器的通用I/O口GPF4、GPF5、GPF6和GPF7分别直接驱动LED D12、D11、D10及D9,而使用GPB0端口驱动蜂鸣器。
4个LED分别在对应端口(GPF4~GPF7)为低电平时发亮,而蜂鸣器在GPB0为高电平时发声。
这5个端口的数据流方向均为输出。
在表7.15中,详细描述了GPF的主要控制寄存器。
GPB的相关寄存器的描述与此类似,具体可以参考S3C2410处理器数据手册。
表7.15 GPF端口(GPF0~GPF7)的主要控制寄存器
26
为了驱动LED和蜂鸣器,首先通过端口配置寄存器将5个相应寄存器配置为输出模式,然后通过对端口数据寄存器的写操作,实现对每个GPIO设备的控制(发亮或发声)。
在7.3.2节中介绍的驱动程序中,s3c2410_gpio_cfgpin()函数和s3c2410_gpio_pullup()函数将进行对某个端口的配置,而s3c2410_gpio_setpin()函数则实现向数据寄存器的某个端口的输出。
7.3.2 GPIO驱动程序
GPIO驱动程序的主要宏定义如下:
/* gpio_drv.h */
#ifndef FS2410_GPIO_SET_H
#define FS2410_GPIO_SET_H
#include <linux/ioctl.h>
27
旗开得胜#define GPIO_DEVICE_NAME "gpio"
#define GPIO_DEVICE_FILENAME "/dev/gpio"
#define LED_NUM 4
#define GPIO_IOCTL_MAGIC 'G'
#define LED_D09_SWT _IOW(GPIO_IOCTL_MAGIC, 0, unsigned int)
#define LED_D10_SWT _IOW(GPIO_IOCTL_MAGIC, 1, unsigned int)
#define LED_D11_SWT _IOW(GPIO_IOCTL_MAGIC, 2, unsigned int)
#define LED_D12_SWT _IOW(GPIO_IOCTL_MAGIC, 3, unsigned int)
#define BEEP_SWT _IOW(GPIO_IOCTL_MAGIC, 4, unsigned int)
#define LED_SWT_ON 0
#define LED_SWT_OFF 1
#define BEEP_SWT_ON 1
#define BEEP_SWT_OFF 0
#endif /* FS2410_GPIO_SET_H */
GPIO驱动的模块加载的部分如下:
static struct file_operations gpio_fops =
{ /* GPIO 设备的file_operations 结构定义*/
.owner = THIS_MODULE,
.open = gpio_open, /* 进行初始化配置*/
.release = gpio_release, /* 关闭设备*/
.read = gpio_read,
.write = gpio_write,
.ioctl = gpio_ioctl, /* 实现主要控制功能*/
};
28
static struct cdev gpio_devs;
static int gpio_init(void)
{
int result;
dev_t dev = MKDEV(major, 0);
if (major)
{ /* 设备号的动态分配*/
result = register_chrdev_region(dev, 1, GPIO_DEVICE_NAME);
}
else
{ /* 设备号的动态分配*/
result = alloc_chrdev_region(&dev, 0, 1, GPIO_DEVICE_NAME);
major = MAJOR(dev);
}
if (result < 0)
{
printk(KERN_WARNING "Gpio: unable to get major %d\n", major);
return result;
}
gpio_setup_cdev(&gpio_devs, 0, &gpio_fops);
printk("The major of the gpio device is %d\n", major);
return 0;
}
GPIO驱动的模块卸载的部分如下:
29
static void gpio_cleanup(void)
{
cdev_del(&gpio_devs); /* 字符设备的注销*/
unregister_chrdev_region(MKDEV(major, 0),1); /* 设备号的注销*/
printk("Gpio device uninstalled\n");
}
module_init(gpio_init);
module_exit(gpio_cleanup);
MODULE_AUTHOR("David");
MODULE_LICENSE("Dual BSD/GPL");
在模块初始化函数中调用的gpio_setup_cdev()函数是用于字符设备的创建和注册函数,其实现代码如下:
static void gpio_setup_cdev(struct cdev *dev, int minor,
struct file_operations *fops)
{ /* 字符设备的创建和注册*/
int err, devno = MKDEV(major, minor);
cdev_init(dev, fops);
dev->owner = THIS_MODULE;
dev->ops = fops;
err = cdev_add (dev, devno, 1);
if (err)
{
printk (KERN_NOTICE "Error %d adding gpio %d", err, minor);
}
30
}
在open()操作函数中,对一些引脚进行配置,而read()和write()操作函数都是空函数。
open()操作函数的实现如下:
int gpio_open (struct inode *inode, struct file *filp)
{ /* open()操作函数:进行寄存器配置*/
s3c2410_gpio_pullup(S3C2410_GPB0, 1); /* BEEP*/
s3c2410_gpio_pullup(S3C2410_GPF4, 1); /* LED D12 */
s3c2410_gpio_pullup(S3C2410_GPF5, 1); /* LED D11 */
s3c2410_gpio_pullup(S3C2410_GPF6, 1); /* LED D10 */
s3c2410_gpio_pullup(S3C2410_GPF7, 1); /* LED D9 */
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_OUTP);
s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF4_OUTP);
s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF5_OUTP);
s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF6_OUTP);
s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF7_OUTP);
return 0;
}
给用户提供的主要工作都在ioctl()操作函数中实现,代码如下:
int switch_gpio(unsigned int pin, unsigned int swt)
{ /* 向5个端口中的一个输出ON/OFF值*/
if (!((pin <= S3C2410_GPF7) && (pin >= S3C2410_GPF4))
&& (pin != S3C2410_GPB0))
{
31
printk("Unsupported pin");
return 1;
}
s3c2410_gpio_setpin(pin, swt);
return 0;
}
static int gpio_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
{ /* ioctl()函数接口:主要接口的实现,对5个GPIO设备进行控制(发亮或发声)*/ unsigned int swt = (unsigned int)arg;
switch (cmd)
{
case LED_D09_SWT:
{
switch_gpio(S3C2410_GPF7, swt); /* LED D9控制*/
}
break;
case LED_D10_SWT:
{
switch_gpio(S3C2410_GPF6, swt); /* LED D10控制*/
}
break;
case LED_D11_SWT:
{
switch_gpio(S3C2410_GPF5, swt); /* LED D11控制*/
32
旗开得胜}
break;
case LED_D12_SWT:
{
switch_gpio(S3C2410_GPF4, swt); /* LED D12控制*/
}
break;
case BEEP_SWT:
{
switch_gpio(S3C2410_GPB0, swt); /* BEEP控制*/
break;
}
default:
{
printk("Unsupported command\n");
break;
}
}
return 0;
}
在release()操作函数中熄灭所有灯和关闭蜂鸣器,实现代码如下:
static int gpio_release(struct inode *node, struct file *file)
在release()操作函数中熄灭所有灯和关闭蜂鸣器,实现代码如下:
33
static int gpio_release(struct inode *node, struct file *file)
{ /* release()操作函数,熄灭所有灯和关闭蜂鸣器*/
switch_gpio(S3C2410_GPB0, BEEP_SWT_OFF);
switch_gpio(S3C2410_GPF4, LED_SWT_OFF);
switch_gpio(S3C2410_GPF5, LED_SWT_OFF);
switch_gpio(S3C2410_GPF6, LED_SWT_OFF);
switch_gpio(S3C2410_GPF7, LED_SWT_OFF);
return 0;
}
配置某个引脚的输入/输出/上拉等功能的代码部分如下:
void s3c2410_gpio_cfgpin(unsigned int pin, unsigned int function)
{ /* 对某个引脚进行配置(输入/输出/其他功能)*/
unsigned long base = S3C2410_GPIO_BASE(pin); /* 获得端口的组基地址*/
unsigned long shift = 1;
unsigned long mask = 0x03; /* 通常用配置寄存器的两位表示一个端口*/
unsigned long con;
unsigned long flags;
if (pin < S3C2410_GPIO_BANKB)
{
shift = 0;
mask = 0x01; /* 在GPA 端口中用配置寄存器的一位表示一个端口*/ }
mask <<= (S3C2410_GPIO_OFFSET(pin) << shift);
34
local_irq_save(flags); /* 保存现场,保证下面一段是原子操作*/
con = __raw_readl(base + 0x00);
con &= ~mask;
con |= function;
__raw_writel(con, base + 0x00); /* 向配置寄存器写入新配置数据*/
local_irq_restore(flags); /* 恢复现场*/
}
void s3c2410_gpio_pullup(unsigned int pin, unsigned int to)
{ /* 配置上拉功能*/
unsigned long base = S3C2410_GPIO_BASE(pin); /* 获得端口的组基地址*/
unsigned long offs = S3C2410_GPIO_OFFSET(pin);/* 获得端口的组内偏移地址*/
unsigned long flags;
unsigned long up;
if (pin < S3C2410_GPIO_BANKB)
{
return;
}
local_irq_save(flags);
up = __raw_readl(base + 0x08);
up &= ~(1 << offs);
up |= to << offs;
__raw_writel(up, base + 0x08); /* 向上拉功能寄存器写入新配置数据*/
local_irq_restore(flags);
}
35
旗开得胜向某个引脚输出数据功能的代码部分如下:
void s3c2410_gpio_setpin(unsigned int pin, unsigned int to)
{ /* 向某个引脚进行输出*/
unsigned long base = S3C2410_GPIO_BASE(pin);
unsigned long offs = S3C2410_GPIO_OFFSET(pin);
unsigned long flags;
unsigned long dat;
local_irq_save(flags);
dat = __raw_readl(base + 0x04);
dat &= ~(1 << offs);
dat |= to << offs;
__raw_writel(dat, base + 0x04); /* 向数据寄存器写入新数据*/
local_irq_restore(flags);
}
下面列出GPIO驱动程序的测试用例:
/* gpio_test.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
36
旗开得胜#include <sys/stat.h>
#include "gpio_drv.h"
int led_timer(int dev_fd, int led_no, unsigned int time)
{ /* 发亮指定LED 一段时间之后熄灭它*/
led_no %= 4;
ioctl(dev_fd, LED_D09_SWT + led_no, LED_SWT_ON); /* 发亮*/
sleep(time);
ioctl(dev_fd, LED_D09_SWT + led_no, LED_SWT_OFF); /* 熄灭*/
}
int beep_timer(int dev_fd, unsigned int time)
{/* 开蜂鸣器一段时间之后关闭*/
ioctl(dev_fd, BEEP_SWT, BEEP_SWT_ON); /* 发声*/
sleep(time);
ioctl(dev_fd, BEEP_SWT, BEEP_SWT_OFF); /* 关闭*/
}
int main()
{
int i = 0;
int dev_fd;
/* 打开GPIO 设备*/
dev_fd = open(GPIO_DEVICE_FILENAME, O_RDWR | O_NONBLOCK);
if ( dev_fd == -1 )
{
printf("Cann't open gpio device file\n");
exit(1);
37
旗开得胜}
while(1)
{
i = (i + 1) % 4;
led_timer(dev_fd, i, 1); /* LED 操作*/
beep_timer(dev_fd, 1); /* BEEP 操作*/
}
close(dev_fd);
return 0;
}
具体运行过程如下,首先编译并加载驱动程序:
$ make clean;make /* 驱动程序的编译*/
$ insmod gpio_drv.ko /* 加载GPIO驱动*/
$ cat /proc/devices /* 通过这个命令可以查到GPIO设备的主设备号*/
$ mknod /dev/gpio c 252 0 /* 假设主设备号为252,创建设备文件节点*/
然后编译并运行驱动测试程序:
$ arm-linux-gcc -o gpio_test gpio_test.c
$ ./gpio_test
运行结果为4个LED轮流闪烁,同时蜂鸣器以一定周期发出声响。
38。