Linux下的C编程实战(五)
――驱动程序设计
1.
引言
设备驱动程序是操作系统内核和机器硬件之间的接口,它为应用程序屏蔽硬件的细节,一般来说,
Linux
的设备驱动程序需要完成如下功能:
(
1
)初始化设备;
(
2
)提供各类设备服务;
(
3
)负责内核和设备之间的数据交换;
(
4
)检测和处理设备工作过程中出现的错误。
妙不可言的是,
Linux
下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得
Windows
的设备操作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。本系列文章的第
2
章文件系统编程中已经看到了这些函数的真面目,它们就是
open ()
、
close ()
、
read ()
、
write ()
等。
Linux
主要将设备分为二类:字符设备和块设备(当然网络设备及
USB
等其它设备的驱动编写方法又稍有不同)。这两类设备的不同点在于:在对字符设备发出读
/
写请求时,实际的硬件
I/O
一般就紧接着发生了,而块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的
I/O
操作。块设备主要针对磁盘等慢速设备。以字符设备的驱动较为简单,因此本章主要阐述字符设备的驱动编写。
2.
驱动模块函数
init
函数用来完成对所控设备的初始化工作,并调用
register_chrdev()
函数注册字符设备。假设有一字符设备“
exampledev
”,则其
init
函数为:
void exampledev_init(void)
{
if (register_chrdev(MAJOR_NUM, " exampledev ", &exampledev_fops))
TRACE_TXT("Device exampledev driver registered error");
else
TRACE_TXT("Device exampledev driver registered successfully");
…//
设备初始化
}
其中,
register_chrdev
函数中的参数
MAJOR_NUM
为主设备号
,
“
exampledev
”为设备名,
exampledev_fops
为包含基本函数入口点的结构体,类型为
file_operations
。当执行
exampledev_init
时,它将调用内核函数
register_chrdev
,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。
file_operations
结构体定义为:
struct file_operations
{
int (*lseek)();
int (*read)();
int (*write)();
int (*readdir)();
int (*select)();
int (*ioctl)();
int (*mmap)();
int (*open)();
void(*release)();
int (*fsync)();
int (*fasync)();
int (*check_media_change)();
void(*revalidate)();
};
大多数的驱动程序只是利用了其中的一部分,对于驱动程序中无需提供的功能,只需要把相应位置的值设为
NULL
。对于字符设备来说,要提供的主要入口有:
open ()
、
release ()
、
read ()
、
write ()
、
ioctl ()
。
open()
函数
对设备特殊文件进行
open()
系统调用时,将调用驱动程序的
open ()
函数:
int open(struct inode * inode ,struct file * file);
其中参数
inode
为设备特殊文件的
inode (
索引结点
)
结构的指针,参数
file
是指向这一设备的文件结构的指针。
open()
的主要任务是确定硬件处在就绪状态、验证次设备号的合法性
(
次设备号可以用
MINOR(inode-> i - rdev)
取得
)
、控制使用设备的进程数、根据执行情况返回状态码
(0
表示成功,负数表示存在错误
)
等;
release()
函数
当最后一个打开设备的用户进程执行
close ()
系统调用时,内核将调用驱动程序的
release ()
函数:
void release (struct inode * inode ,struct file * file) ;
release
函数的主要任务是清理未结束的输入
/
输出操作、释放资源、用户自定义排他标志的复位等。
read()
函数
当对设备特殊文件进行
read()
系统调用时,将调用驱动程序
read()
函数:
void read(struct inode * inode ,struct file * file ,char * buf ,int count) ;
参数
buf
是指向用户空间缓冲区的指针,由用户进程给出,
count
为用户进程要求读取的字节数,也由用户给出。
read()
函数的功能就是从硬设备或内核内存中读取或复制
count
个字节到
buf
指定的缓冲区中。在复制数据时要注意,驱动程序运行在内核中,而
buf
指定的缓冲区在用户内存区中,是不能直接在内核中访问使用的,因此,必须使用特殊的复制函数来完成复制工作,这些函数在
<asm/ segment.h>
中定义:
void put_user_byte (char data_byte ,char * u_addr) ;
void put_user_word (short data_word ,short * u_addr) ;
void put_user_long(long data_long ,long * u_addr) ;
void memcpy_tofs (void * u_addr ,void * k_addr ,unsigned long cnt) ;
参数
u_addr
为用户空间地址,
k_addr
为内核空间地址,
cnt
为字节数。
write( )
函数
当设备特殊文件进行
write ()
系统调用时,将调用驱动程序的
write ()
函数:
void write (struct inode * inode ,struct file * file ,char * buf ,int count) ;
write ()
的功能是将参数
buf
指定的缓冲区中的
count
个字节内容复制到硬件或内核内存中,和
read()
一样,复制工作也需要由特殊函数来完成:
unsigned char_get_user_byte (char * u_addr) ;
unsigned char_get_user_word (short * u_addr) ;
unsigned char_get_user_long(long * u_addr) ;
unsigned memcpy_fromfs(void * k_addr ,void * u_addr ,unsigned long cnt) ;
ioctl()
函数
该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:
int ioctl (struct inode * inode ,struct file * file ,unsigned int cmd ,unsigned long arg);
参数
cmd
为设备驱动程序要执行的命令的代码,由用户自定义,参数
arg
为相应的命令提供参数,类型可以是整型、指针等。
同样,在驱动程序中,这些函数的定义也必须符合命名规则,按照本文约定,设备“
exampledev
”的驱动程序的这些函数应分别命名为
exampledev_open
、
exampledev_ release
、
exampledev_read
、
exampledev_write
、
exampledev_ioctl
,因此设备“
exampledev
”的基本入口点结构变量
exampledev_fops
赋值如下:
struct file_operations exampledev_fops {
NULL ,
exampledev_read ,
exampledev_write ,
NULL ,
NULL ,
exampledev_ioctl ,
NULL ,
exampledev_open ,
exampledev_release ,
NULL ,
NULL ,
NULL ,
NULL
} ;
3.
内存分配
由于
Linux
驱动程序在内核中运行,因此在设备驱动程序需要申请
/
释放内存时,不能使用用户级的
malloc/free
函数,而需由内核级的函数
kmalloc/kfree ()
来实现,
kmalloc()
函数的原型为:
void kmalloc (size_t size ,int priority);
参数
size
为申请分配内存的字节数;参数
priority
说明若
kmalloc()
不能马上分配内存时用户进程要采用的动作:
GFP_KERNEL
表示等待,即等
kmalloc()
函数将一些内存安排到交换区来满足你的内存需要,
GFP_ATOMIC
表示不等待,如不能立即分配到内存则返回
0
值;函数的返回值指向已分配内存的起始地址,出错时,返回
0
。
kmalloc ()
分配的内存需用
kfree()
函数来释放,
kfree ()
被定义为:
# define kfree (n) kfree_s( (n) ,0)
其中
kfree_s ()
函数原型为:
void kfree_s (void * ptr ,int size);
参数
ptr
为
kmalloc()
返回的已分配内存的指针,
size
是要释放内存的字节数,若为
0
时,由内核自动确定内存的大小。
4.
中断
许多设备涉及到中断操作,因此,在这样的设备的驱动程序中需要对硬件产生的中断请求提供中断服务程序。与注册基本入口点一样,驱动程序也要请求内核将特定的中断请求和中断服务程序联系在一起。在
Linux
中,用
request_irq()
函数来实现请求:
int request_irq (unsigned int irq ,void( * handler) int ,unsigned long type ,char * name);
参数
irq
为要中断请求号,参数
handler
为指向中断服务程序的指针,参数
type
用来确定是正常中断还是快速中断(正常中断指中断服务子程序返回后,内核可以执行调度程序来确定将运行哪一个进程;而快速中断是指中断服务子程序返回后,立即执行被中断程序,正常中断
type
取值为
0
,快速中断
type
取值为
SA_INTERRUPT
),参数
name
是设备驱动程序的名称。
5.
实例
笔者最近设计了一块采用三星
S3C2410 ARM
处理器的电路板(
ARM
处理器广泛应用于手机、
PDA
等嵌入式系统),板上包含四个用户可编程的发光二极管(
LED
),这些
LED
连接在
ARM
处理器的可编程
I/O
口(
GPIO
)上。下图给出了
ARM
中央处理器与
LED
的连接原理:
< coordsize="21600,21600" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f" style="margin: 0px; padding: 0px; max-width: 100%;">
我们在
ARM
处理器上移植
Linux
操作系统,现在来编写这些
LED
的驱动:
#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/spinlock.h>
#include <linux/irq.h>
#include <linux/delay.h>
#include <asm/hardware.h>
#define DEVICE_NAME "leds" /*
定义
led
设备的名字
*/
#define LED_MAJOR 231 /*
定义
led
设备的主设备号
*/
static unsigned long led_table[] =
{
/*I/O
方式
led
设备对应的硬件资源
*/
GPIO_B10, GPIO_B8, GPIO_B5, GPIO_B6,
};
/*
使用
ioctl
控制
led*/
static int leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd,
unsigned long arg)
{
switch (cmd)
{
case 0:
case 1:
if (arg > 4)
{
return -EINVAL;
}
write_gpio_bit(led_table[arg], !cmd);
default:
return -EINVAL;
}
}
static struct file_operations leds_fops =
{
owner: THIS_MODULE, ioctl: leds_ioctl,
};
static devfs_handle_t devfs_handle;
static int __init leds_init(void)
{
int ret;
int i;
/*
在内核中注册设备
*/
ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops);
if (ret < 0)
{
printk(DEVICE_NAME " can't register major number\n");
return ret;
}
devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT, LED_MAJOR,
0, S_IFCHR | S_IRUSR | S_IWUSR, &leds_fops, NULL);
/*
使用宏进行端口初始化,
set_gpio_ctrl
和
write_gpio_bit
均为宏定义
*/
for (i = 0; i < 8; i++)
{
set_gpio_ctrl(led_table[i] | GPIO_PULLUP_EN | GPIO_MODE_OUT);
write_gpio_bit(led_table[i], 1);
}
printk(DEVICE_NAME " initialized\n");
return 0;
}
static void __exit leds_exit(void)
{
devfs_unregister(devfs_handle);
unregister_chrdev(LED_MAJOR, DEVICE_NAME);
}
module_init(leds_init);
module_exit(leds_exit);
使用命令方式编译
led
驱动模块:
#arm-linux-gcc -D__KERNEL__ -I/arm/kernel/include
-DKBUILD_BASENAME=leds -DMODULE -c -o leds.o leds.c
以上命令将生成
leds.o
文件,把该文件复制到板子的
/lib
目录下,使用以下命令就可以安装
leds
驱动模块:
#insmod /lib/ leds.o
删除该模块的命令是:
#rmmod leds
6.
小结
本章讲述了
Linux
设备驱动程序的入口函数及驱动程序中的内存申请、中断等,并给出了一个通过
ARM
处理器的
GPIO
口控制
LED
的驱动实例。
本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120038,如需转载请自行联系原作者