目录
一:概述
驱动程序是软件与硬件之间的抽象层;因此,它需要与这两者对话,本文将向你展示驱动程序如何与硬件对话。并介绍I/O端口和I/O内存的概念。
二:I/O端口和I/O内存的概念
CPU通过写入和读取硬件寄存器的方式来控制硬件设备。在大多数情况下,一个硬件设备有多个寄存器,可以通过连续的地址访问它们,我们将这些可被CPU读写的硬件寄存器称为I/O端口。在硬件层面,访问I/O端口是通过地址总线和控制总线发送读写控制信号,以及通过数据总线读取或写入数据来访问的。
一些CPU制造商在其芯片内部采用统一的地址空间,认为访问设备寄存器和访问内存的方式相同,使用同一地址空间即可。而另一些则认为硬件设备不同于内存,因此应该有单独的地址空间,比如像最著名的x86系列就为I/O地址设置了独立的读写电气线路,并为访问改地址设置了特殊的CPU指令。出于上述原因,Linux在其运行的所有平台上都实现了I/O端口的概念,即使在使用统一地址空间的平台上也是如此。
然而上面说的I/O端口并不是设备访问的唯一方式,目前大多数PCI设备都将硬件寄存器映射到内存地址区域,我们将这样的内存区域称为I/O内存,即可以像访问普通内存一样来访问设备,因此它不需要使用特殊的处理器指令,CPU访问内存的效率更高,而且编译器在访问内存时,在寄存器分配和寻址方式选择方面有更大的自由度。
三:硬件寄存器(I/O寄存器)和内存
尽管硬件寄存器和内存之间有很大的相似性,但程序员访问I/O寄存器时必须小心谨慎,避免被CPU或编译器的优化所欺骗,因为这些优化会改变预期的I/O行为。
I/O寄存器和内存的主要区别在于,CPU或编译器的优化会对I/O操作产生副作用,比如正常的内存写入操作是将一个值存储到一个位置,而内存读取则返回最后写入的值。但是由于内存访问速度对CPU性能至关重要,所以CPU或编译器通过多种方式对内存访问进行了优化,比如值被缓存,读/写指令被重新排序等。
然而这些优化往往对I/O操作是有害的。处理器无法预料其他进程对I/O寄存器访问顺序的依赖情况,编译器或CPU可能会耍小聪明,重新安排你所请求的操作顺序;结果可能会出现难以调试的奇怪错误,因此驱动程序确保在访问寄存器时不执行缓存,也不进行读写重新排序。
硬件缓存的问题最容易解决:底层硬件已经被配置(自动配置或由Linux初始化代码配置),以便在访问I/O区域(无论时内存还是端口区域)时禁用任何硬件缓存。
编译器优化和读写指令重排的解决方案是,在必要的地方设置内存屏障,以使得硬件或处理器保证以特定的顺序得到之前操作的结果。Linux提供了四个函数来保证所有可能的排序需求:
#include <linux/kernel.h>
void barrier(void)
上面这个barrier函数告诉编译器插入内存屏障,防止编译器跨屏障进行优化,在屏障内读写操作可以自由地进行重新排序,对硬件没有影响。编译后的代码会将所有当前已修改并驻留在CPU寄存器中的值存储到内存中,并在以后需要时重新读取。
#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
上面这些函数在编译指令流中插入硬件内存屏障;它们的实现取决于具体的平台(x86, arm,mips...), rmb(读内存屏障)确保在执行任何后续读操作之前,在屏障之前出现的任何读操作都均已完成。wmb(写内存屏障)是保证在执行任何后续写操作之前,在屏障之前出现的任何写操作均已完成。mb是兼具上面两者的功能。read_barrier_depends 是一种特殊的,较弱的读屏障形式,不常用。
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
上面这些函数是当内核为SMP(对称多处理器)系统编译时使用的,当不是SMP(对称多处理器)系统时,使用再之前的普通内存屏障。
驱动程序中内存屏障的典型用法像下面这样:
writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);
wmb( );
writel(dev->registers.control, DEV_GO);
如前所述,必须确保所有对设备寄存器的操作都已按正确设置之后,然后才能开始设备寄存器做新的操作。
内存屏障会强制要求安装必要的顺序完成写入操作。由于内存屏障会影响性能,因此只应在真正需要时使用。不同类型的内存屏障也有不同的性能特点,因此尽可能使用恰当的类型,例如,在x86平台上,wmb()目前不起任何作用,因此处理器外的写入不会重新排序,不过,读取时重新排序的,因此mb()比wmb()慢。
值得注意的是,处理同步问题的大多数其他内核原语,如spinlock和atomic_t操作,也具有内存屏障的功能。同样值得注意的是,一些外设总线(如PCI总线)本身也有缓存问题,我们将在后面章节讨论这些问题。有些架构允许将赋值和内存屏障有效结合起来使用。内核提供了一些宏来使用这种组合,让驱动开发者使用更方便;它们的定义如下:
#define set_mb(var, value) do {var = value; mb( );} while 0
#define set_wmb(var, value) do {var = value; wmb( );} while 0
#define set_rmb(var, value) do {var = value; rmb( );} while 0
四:使用I/O端口
I/O端口是驱动程序与设备通信的手段,至少在过去是如此。本节将介绍使用I/O端口的各种函数;并且还将讨论一些可移植性问题。
1. 分配I/O端口
一般来说在访问端口之前,一定要确保对端口拥有独占访问权,否则不应该访问 I/O 端口。内核提供了一个函数,允许驱动程序申请端口。该函数接口是 request_region:
#include <linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n,
const char *name)
该函数的含义是,向内核申请 n 个端口,从first开始。name 为设备名称。如果分配成功,返回值为非 NULL。如果 request_region 返回 NULL,则无法使用所需的端口。
所有已分配的端口都显示在 /proc/ioports。如果无法分配到所需的端口,就可以到这里看看是谁先在占用着这些端口。
当I/O端口在使用完后,或在模块卸载时,应该将它们归还给系统:
void release_region(unsigned long start, unsigned long n);
2. 在驱动中使用I/O端口
当成功申请了I/O端口后,就须对这些端口进行读取或写入了。大多数硬件会区分 8 位、16 位和 32 位端口。因此,C 程序必须调用不同的函数来访问不同大小的端口,在只支持内存映射 I/O 的计算机体系结构通过将端口地址重映射到内存地址来伪造端口 I/O,内核会向驱动程序隐藏这些细节。Linux内核提供了以下函数来访问 I/O 端口:
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
上面这组函数用来访问8位端口(1个字节)。参数 port 在某些平台上被定义为unsigned long,而在其他平台上则被定义为unsigned short。inb 的返回类型在不同架构下也有所不同。
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
上面这组函数用来访问16位端口(2个字节);在某些嵌入式平台上,这些函数不可用,因为该平台只支持字节 I/O。
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
上面这组函数用来访问32位端口(4个字节);在某些嵌入式平台上,这些函数不可用,因为只支持字节I/O。
3.在用户空间中使用I/O端口
刚才介绍的函数主要供设备驱动程序使用,但也可以在用户空间使用,至少在 PC 级计算机上是如此。GNU C 库在 <sys/io.h> 中定义了这些函数。要在用户空间代码中使用这些函数,必须满足以下条件:
(1)编译程序时必须使用 -O 选项,以强制扩展内联函数。
(2)必须使用 ioperm 或 iopl 系统调用来获取I/O端口执行权限。ioperm 获取单个端口的权限,而 iopl 则获取整个 I/O 空间的权限。这两个函数都是 x86 专用的。
(3)程序必须以 root 运行才能调用 ioperm 或 iopl。或程序以 root 方式获得端口访问权限。
如果不是x86平台,则没有 ioperm 和 iopl 系统调用,但用户空间仍可使用 /dev/port 设备文件访问 I/O 端口。但要注意的是,该文件的含义也与特定平台密切相关,/dev/prot也仅限于在PC上使用。
4. I/O串操作(一次读写多个字节)
除了在端口上输入和输出字节/2字节/4字节之外,一些处理器还实现了特殊指令,用于在端口端传输多个字节序列。这些指令就是所谓的字符串指令,它们比 C 语言循环更快地完成任务。这些函数实现了字符串 I/O 的概念,它们或者使用一条机器指令,或者在目标处理器没有执行字符串 I/O 的指令时执行一个紧凑的循环。在一些嵌入式平台上,没有定义这些宏。所以这些宏不是跨平台的,不具有可移植。只能在某些平台上使用,比如PC上。以下是这些函数的定义
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
从内存地址 addr 开始读取 count 字节数据并将其写入到端口 port 中。从端口读出 count 字节数据并将其写入到内存地址 addr 中。
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
从内存地址 addr 上开始读取 count 个字(16bit)数据并将其写入到端口 port 中。从端口读出 count个2字节(16bit)数据并将其写入到内存地址 addr 中。
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
从内存地址 addr 上开始读取 count 个4字节(32bit)数据并将其写入到端口 port 中。从端口读出 count 个4字节(32bit)数据并将其写入到内存地址 addr 中。
在使用这些串读写函数时,有一点需要注意:当端口和主机系统的字节顺序不同时(大小端), 如果直接对端口进行读写,结果可能会是不正确的。为此,需要使用 inwsw 函数,在读写端口时要对转换字节顺序,以便使得读取的字节顺序与主机的顺序一致。
5. 暂停I/O读写 (停顿等待)
在某些平台上(最明显的是 i386),当一个进程试图过快地向总线读写传输输数据时会产生问题,因为处理器的处理速度远大于总线传输速度,造成读写速度不匹配,会产生数据丢失问题,尤其是当设备速度太慢时。解决方法是在连续的I/O指令流里,在每条 I/O 指令后面插入一个小的延迟指令。在 x86 上,可以通过向端口写入 0x80指令,或执行忙等待来实现。更多这方面的详细信息,请参阅平台的的 io.h 文件。
如果你担心设备丢失了一些数据,或者你担心它可能会丢失一些数据,你可以使用暂停函数来代替正常函数。暂停函数与前面列出的函数完全相同,但它们的名称以 _p 结尾;它们被称为 inb_p、outb_p 等。这些函数是被大多数平台支持的。
6. I/O操作对平台的依赖
I/O 指令在本质上高度依赖处理器。由于这些指令涉及处理器如何处理数据进出的细节,因此很难掩盖不同系统之间的差异。因此,大部分与端口 I/O 相关的源代码都与平台有关。
通过回看前面的函数列表,你可以看到其中一个不兼容的地方,即数据类型,在那些函数列表中,参数的类型根据不同平台的架构差异而有所不同。例如,端口在 x86(处理器支持 64-KB I/O 空间)上是无符号短端口,而在其他平台上则是无符号长端口。
平台依赖性源于处理器的基本结构差异,因此是不可避免的。我们不会详细讨论这些差异,因为我们假设你不会在不了解底层硬件的情况下为特定系统编写设备驱动程序。下面将概述内核对各平台的支持情况:
IA-32 (x86)
x86_64
支持本文所述的所有函数。端口号为无符号短整型。支持串操作指令。
IA-64(Itanium)
支持本文所述所有函数;端口号为无符号长整型。端口是内存映射的。支持串操作指令
Alpha
支持所有函数,端口是内存映射的。在不同的 Alpha 平台上,端口 I/O 的实现会根据其 使用的芯片组而有所不同。支持串操作指令
ARM
支持所有函数,端口是内存映射的。支持串操作指令。
MIPS
MIPS64
支持所有功能。由于缺乏I/O串操作指令,因此串操作是通过紧凑的汇编循环实现的。
以上就不一一列举了,每个平台上如何执行 I/O 操作,在每个平台的程序员手册中都有详细描述;这些手册通常可以在网上以 PDF 格式下载。
7. 通用 I/O 端口概述
我们即将展示一个在设备驱动程序使用I/O端口的例子,该例子使用的是通用数字I/O端口,这些端口在大多数计算机系统中都可以找到。
通用数字I/O端口是一个按字节读写的,可以是内存映射(memory-mapped),也可以是端口映射(port-mapped)。当向输出端口写入数值时,输出引脚上的电信号会根据写入的各个位发生变化。当从输入端口读取数值时,输入引脚上的电信号将以单个比特值的形式返回。
通用数字 I/O 端口的实际实现和软件接口因系统而异,通常情况下,I/O引脚由两个I/O寄存器控制:一个用于选择哪些引脚用作输入,哪些引脚用作输出;另一个用于实际读取或写入逻辑电平。然而有时候事情会变得更加简单,这些位要么被硬接线为输入位,要么被硬接线为输出位(但在这种情况下,它们不再被称为“通用I/O”)。
8. 并口概述
所有个人计算机(x86)上都配备了并口,这是一种不太通用的I/O端口。在个人计算机(x86)上,并口是运行我们示例代码的首先外设接口。尽管大多数读者可能都有并行端口说明,但为了方便起见,我们在此对并口进行总结。
并行接口的最小配置是由三个 8 位端口组成。PC 标准将第一个并行接口的 I/O 端口设置在 0x378,将第二个并行接口的 I/O 端口设置在 0x278。第一个端口是双向数据寄存器,直接连接到物理连接器上的引脚 2-9。第二个端口是只读状态寄存器;当并行端口用于打印机时,该寄存器报告打印机状态的几个方面,如联机、缺纸或繁忙。第三个端口是一个只输出控制寄存器,除其他外,它可以控制是否启用中断。
并行通信中使用的信号电平是标准的TTL电平:0 和 5 伏,逻辑阈值约为 1.2 伏。尽管大多数现代并行端口在电流和电压额定值方面都更胜一筹,但你至少可以指定端口符合标准 TTL LS 额定电流。 位规格如图 9-1 所示。唯一没有相关信号引脚的位是端口 2 的第 4 位(0x10),用它配置并行端口的中断。
9. 一个简单的驱动例子
/*
* short.c -- Simple Hardware Operations and Raw Tests
* short.c -- also a brief example of interrupt handling ("short int")
*
* Copyright (C) 2001 Alessandro Rubini and Jonathan Corbet
* Copyright (C) 2001 O'Reilly & Associates
*
* The source code in this file can be freely used, adapted,
* and redistributed in source or binary form, so long as an
* acknowledgment appears in derived source files. The citation
* should list that the code comes from the book "Linux Device
* Drivers" by Alessandro Rubini and Jonathan Corbet, published
* by O'Reilly & Associates. No warranty is attached;
* we cannot take responsibility for errors or fitness for use.
*
* $Id: short.c,v 1.16 2004/10/29 16:45:40 corbet Exp $
*/
/*
* FIXME: this driver is not safe with concurrent readers or
* writers.
*/
#include <linux/config.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h> /* printk() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/delay.h> /* udelay */
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/ioport.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/poll.h>
#include <linux/wait.h>
#include <asm/io.h>
#define SHORT_NR_PORTS 8 /* use 8 ports by default */
/*
* all of the parameters have no "short_" prefix, to save typing when
* specifying them at load time
*/
static int major = 0; /* dynamic by default */
module_param(major, int, 0);
static int use_mem = 0; /* default is I/O-mapped */
module_param(use_mem, int, 0);
/* default is the first printer port on PC's. "short_base" is there too
because it's what we want to use in the code */
static unsigned long base = 0x378;
unsigned long short_base = 0;
module_param(base, long, 0);
/* The interrupt line is undefined by default. "short_irq" is as above */
static int irq = -1;
volatile int short_irq = -1;
module_param(irq, int, 0);
static int probe = 0; /* select at load time how to probe irq line */
module_param(probe, int, 0);
static int wq = 0; /* select at load time whether a workqueue is used */
module_param(wq, int, 0);
static int tasklet = 0; /* select whether a tasklet is used */
module_param(tasklet, int, 0);
static int share = 0; /* select at load time whether install a shared irq */
module_param(share, int, 0);
MODULE_AUTHOR ("Alessandro Rubini");
MODULE_LICENSE("Dual BSD/GPL");
unsigned long short_buffer = 0;
unsigned long volatile short_head;
volatile unsigned long short_tail;
DECLARE_WAIT_QUEUE_HEAD(short_queue);
/* Set up our tasklet if we're doing that. */
void short_do_tasklet(unsigned long);
DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);
/*
* Atomicly increment an index into short_buffer
*/
static inline void short_incr_bp(volatile unsigned long *index, int delta)
{
unsigned long new = *index + delta;
barrier(); /* Don't optimize these two together */
*index = (new >= (short_buffer + PAGE_SIZE)) ? short_buffer : new;
}
/*
* The devices with low minor numbers write/read burst of data to/from
* specific I/O ports (by default the parallel ones).
*
* The device with 128 as minor number returns ascii strings telling
* when interrupts have been received. Writing to the device toggles
* 00/FF on the parallel data lines. If there is a loopback wire, this
* generates interrupts.
*/
int short_open (struct inode *inode, struct file *filp)
{
extern struct file_operations short_i_fops;
if (iminor (inode) & 0x80)
filp->f_op = &short_i_fops; /* the interrupt-driven node */
return 0;
}
int short_release (struct inode *inode, struct file *filp)
{
return 0;
}
/* first, the port-oriented device */
enum short_modes {SHORT_DEFAULT=0, SHORT_PAUSE, SHORT_STRING, SHORT_MEMORY};
ssize_t do_short_read (struct inode *inode, struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
int retval = count, minor = iminor (inode);
unsigned long port = short_base + (minor&0x0f);
void *address = (void *) short_base + (minor&0x0f);
int mode = (minor&0x70) >> 4;
unsigned char *kbuf = kmalloc(count, GFP_KERNEL), *ptr;
if (!kbuf)
return -ENOMEM;
ptr = kbuf;
if (use_mem)
mode = SHORT_MEMORY;
switch(mode) {
case SHORT_STRING:
insb(port, ptr, count);
rmb();
break;
case SHORT_DEFAULT:
while (count--) {
*(ptr++) = inb(port);
rmb();
}
break;
case SHORT_MEMORY:
while (count--) {
*ptr++ = ioread8(address);
rmb();
}
break;
case SHORT_PAUSE:
while (count--) {
*(ptr++) = inb_p(port);
rmb();
}
break;
default: /* no more modes defined by now */
retval = -EINVAL;
break;
}
if ((retval > 0) && copy_to_user(buf, kbuf, retval))
retval = -EFAULT;
kfree(kbuf);
return retval;
}
/*
* Version-specific methods for the fops structure. FIXME don't need anymore.
*/
ssize_t short_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
return do_short_read(filp->f_dentry->d_inode, filp, buf, count, f_pos);
}
ssize_t do_short_write (struct inode *inode, struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
int retval = count, minor = iminor(inode);
unsigned long port = short_base + (minor&0x0f);
void *address = (void *) short_base + (minor&0x0f);
int mode = (minor&0x70) >> 4;
unsigned char *kbuf = kmalloc(count, GFP_KERNEL), *ptr;
if (!kbuf)
return -ENOMEM;
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
ptr = kbuf;
if (use_mem)
mode = SHORT_MEMORY;
switch(mode) {
case SHORT_PAUSE:
while (count--) {
outb_p(*(ptr++), port);
wmb();
}
break;
case SHORT_STRING:
outsb(port, ptr, count);
wmb();
break;
case SHORT_DEFAULT:
while (count--) {
outb(*(ptr++), port);
wmb();
}
break;
case SHORT_MEMORY:
while (count--) {
iowrite8(*ptr++, address);
wmb();
}
break;
default: /* no more modes defined by now */
retval = -EINVAL;
break;
}
kfree(kbuf);
return retval;
}
ssize_t short_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
return do_short_write(filp->f_dentry->d_inode, filp, buf, count, f_pos);
}
unsigned int short_poll(struct file *filp, poll_table *wait)
{
return POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM;
}
struct file_operations short_fops = {
.owner = THIS_MODULE,
.read = short_read,
.write = short_write,
.poll = short_poll,
.open = short_open,
.release = short_release,
};
/* then, the interrupt-related device */
ssize_t short_i_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
int count0;
DEFINE_WAIT(wait);
while (short_head == short_tail) {
prepare_to_wait(&short_queue, &wait, TASK_INTERRUPTIBLE);
if (short_head == short_tail)
schedule();
finish_wait(&short_queue, &wait);
if (signal_pending (current)) /* a signal arrived */
return -ERESTARTSYS; /* tell the fs layer to handle it */
}
/* count0 is the number of readable data bytes */
count0 = short_head - short_tail;
if (count0 < 0) /* wrapped */
count0 = short_buffer + PAGE_SIZE - short_tail;
if (count0 < count) count = count0;
if (copy_to_user(buf, (char *)short_tail, count))
return -EFAULT;
short_incr_bp (&short_tail, count);
return count;
}
ssize_t short_i_write (struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
int written = 0, odd = *f_pos & 1;
unsigned long port = short_base; /* output to the parallel data latch */
void *address = (void *) short_base;
if (use_mem) {
while (written < count)
iowrite8(0xff * ((++written + odd) & 1), address);
} else {
while (written < count)
outb(0xff * ((++written + odd) & 1), port);
}
*f_pos += count;
return written;
}
struct file_operations short_i_fops = {
.owner = THIS_MODULE,
.read = short_i_read,
.write = short_i_write,
.open = short_open,
.release = short_release,
};
irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct timeval tv;
int written;
do_gettimeofday(&tv);
/* Write a 16 byte record. Assume PAGE_SIZE is a multiple of 16 */
written = sprintf((char *)short_head,"%08u.%06u\n",
(int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
BUG_ON(written != 16);
short_incr_bp(&short_head, written);
wake_up_interruptible(&short_queue); /* awake any reading process */
return IRQ_HANDLED;
}
/*
* The following two functions are equivalent to the previous one,
* but split in top and bottom half. First, a few needed variables
*/
#define NR_TIMEVAL 512 /* length of the array of time values */
struct timeval tv_data[NR_TIMEVAL]; /* too lazy to allocate it */
volatile struct timeval *tv_head=tv_data;
volatile struct timeval *tv_tail=tv_data;
static struct work_struct short_wq;
int short_wq_count = 0;
/*
* Increment a circular buffer pointer in a way that nobody sees
* an intermediate value.
*/
static inline void short_incr_tv(volatile struct timeval **tvp)
{
if (*tvp == (tv_data + NR_TIMEVAL - 1))
*tvp = tv_data; /* Wrap */
else
(*tvp)++;
}
void short_do_tasklet (unsigned long unused)
{
int savecount = short_wq_count, written;
short_wq_count = 0; /* we have already been removed from the queue */
/*
* The bottom half reads the tv array, filled by the top half,
* and prints it to the circular text buffer, which is then consumed
* by reading processes
*/
/* First write the number of interrupts that occurred before this bh */
written = sprintf((char *)short_head,"bh after %6i\n",savecount);
short_incr_bp(&short_head, written);
/*
* Then, write the time values. Write exactly 16 bytes at a time,
* so it aligns with PAGE_SIZE
*/
do {
written = sprintf((char *)short_head,"%08u.%06u\n",
(int)(tv_tail->tv_sec % 100000000),
(int)(tv_tail->tv_usec));
short_incr_bp(&short_head, written);
short_incr_tv(&tv_tail);
} while (tv_tail != tv_head);
wake_up_interruptible(&short_queue); /* awake any reading process */
}
irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
/* Grab the current time information. */
do_gettimeofday((struct timeval *) tv_head);
short_incr_tv(&tv_head);
/* Queue the bh. Don't worry about multiple enqueueing */
schedule_work(&short_wq);
short_wq_count++; /* record that an interrupt arrived */
return IRQ_HANDLED;
}
/*
* Tasklet top half
*/
irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
do_gettimeofday((struct timeval *) tv_head); /* cast to stop 'volatile' warning */
short_incr_tv(&tv_head);
tasklet_schedule(&short_tasklet);
short_wq_count++; /* record that an interrupt arrived */
return IRQ_HANDLED;
}
irqreturn_t short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int value, written;
struct timeval tv;
/* If it wasn't short, return immediately */
value = inb(short_base);
if (!(value & 0x80))
return IRQ_NONE;
/* clear the interrupting bit */
outb(value & 0x7F, short_base);
/* the rest is unchanged */
do_gettimeofday(&tv);
written = sprintf((char *)short_head,"%08u.%06u\n",
(int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
short_incr_bp(&short_head, written);
wake_up_interruptible(&short_queue); /* awake any reading process */
return IRQ_HANDLED;
}
void short_kernelprobe(void)
{
int count = 0;
do {
unsigned long mask;
mask = probe_irq_on();
outb_p(0x10,short_base+2); /* enable reporting */
outb_p(0x00,short_base); /* clear the bit */
outb_p(0xFF,short_base); /* set the bit: interrupt! */
outb_p(0x00,short_base+2); /* disable reporting */
udelay(5); /* give it some time */
short_irq = probe_irq_off(mask);
if (short_irq == 0) { /* none of them? */
printk(KERN_INFO "short: no irq reported by probe\n");
short_irq = -1;
}
/*
* if more than one line has been activated, the result is
* negative. We should service the interrupt (no need for lpt port)
* and loop over again. Loop at most five times, then give up
*/
} while (short_irq < 0 && count++ < 5);
if (short_irq < 0)
printk("short: probe failed %i times, giving up\n", count);
}
irqreturn_t short_probing(int irq, void *dev_id, struct pt_regs *regs)
{
if (short_irq == 0) short_irq = irq; /* found */
if (short_irq != irq) short_irq = -irq; /* ambiguous */
return IRQ_HANDLED;
}
void short_selfprobe(void)
{
int trials[] = {3, 5, 7, 9, 0};
int tried[] = {0, 0, 0, 0, 0};
int i, count = 0;
/*
* install the probing handler for all possible lines. Remember
* the result (0 for success, or -EBUSY) in order to only free
* what has been acquired
*/
for (i = 0; trials[i]; i++)
tried[i] = request_irq(trials[i], short_probing,
SA_INTERRUPT, "short probe", NULL);
do {
short_irq = 0; /* none got, yet */
outb_p(0x10,short_base+2); /* enable */
outb_p(0x00,short_base);
outb_p(0xFF,short_base); /* toggle the bit */
outb_p(0x00,short_base+2); /* disable */
udelay(5); /* give it some time */
/* the value has been set by the handler */
if (short_irq == 0) { /* none of them? */
printk(KERN_INFO "short: no irq reported by probe\n");
}
/*
* If more than one line has been activated, the result is
* negative. We should service the interrupt (but the lpt port
* doesn't need it) and loop over again. Do it at most 5 times
*/
} while (short_irq <=0 && count++ < 5);
/* end of loop, uninstall the handler */
for (i = 0; trials[i]; i++)
if (tried[i] == 0)
free_irq(trials[i], NULL);
if (short_irq < 0)
printk("short: probe failed %i times, giving up\n", count);
}
/* Finally, init and cleanup */
int short_init(void)
{
int result;
/*
* first, sort out the base/short_base ambiguity: we'd better
* use short_base in the code, for clarity, but allow setting
* just "base" at load time. Same for "irq".
*/
short_base = base;
short_irq = irq;
/* Get our needed resources. */
if (!use_mem) {
if (! request_region(short_base, SHORT_NR_PORTS, "short")) {
printk(KERN_INFO "short: can't get I/O port address 0x%lx\n",
short_base);
return -ENODEV;
}
} else {
if (! request_mem_region(short_base, SHORT_NR_PORTS, "short")) {
printk(KERN_INFO "short: can't get I/O mem address 0x%lx\n",
short_base);
return -ENODEV;
}
/* also, ioremap it */
short_base = (unsigned long) ioremap(short_base, SHORT_NR_PORTS);
/* Hmm... we should check the return value */
}
/* Here we register our device - should not fail thereafter */
result = register_chrdev(major, "short", &short_fops);
if (result < 0) {
printk(KERN_INFO "short: can't get major number\n");
release_region(short_base,SHORT_NR_PORTS); /* FIXME - use-mem case? */
return result;
}
if (major == 0) major = result; /* dynamic */
short_buffer = __get_free_pages(GFP_KERNEL,0); /* never fails */ /* FIXME */
short_head = short_tail = short_buffer;
/*
* Fill the workqueue structure, used for the bottom half handler.
* The cast is there to prevent warnings about the type of the
* (unused) argument.
*/
/* this line is in short_init() */
INIT_WORK(&short_wq, (void (*)(void *)) short_do_tasklet, NULL);
/*
* Now we deal with the interrupt: either kernel-based
* autodetection, DIY detection or default number
*/
if (short_irq < 0 && probe == 1)
short_kernelprobe();
if (short_irq < 0 && probe == 2)
short_selfprobe();
if (short_irq < 0) /* not yet specified: force the default on */
switch(short_base) {
case 0x378: short_irq = 7; break;
case 0x278: short_irq = 2; break;
case 0x3bc: short_irq = 5; break;
}
/*
* If shared has been specified, installed the shared handler
* instead of the normal one. Do it first, before a -EBUSY will
* force short_irq to -1.
*/
if (short_irq >= 0 && share > 0) {
result = request_irq(short_irq, short_sh_interrupt,
SA_SHIRQ | SA_INTERRUPT,"short",
short_sh_interrupt);
if (result) {
printk(KERN_INFO "short: can't get assigned irq %i\n", short_irq);
short_irq = -1;
}
else { /* actually enable it -- assume this *is* a parallel port */
outb(0x10, short_base+2);
}
return 0; /* the rest of the function only installs handlers */
}
if (short_irq >= 0) {
result = request_irq(short_irq, short_interrupt,
SA_INTERRUPT, "short", NULL);
if (result) {
printk(KERN_INFO "short: can't get assigned irq %i\n",
short_irq);
short_irq = -1;
}
else { /* actually enable it -- assume this *is* a parallel port */
outb(0x10,short_base+2);
}
}
/*
* Ok, now change the interrupt handler if using top/bottom halves
* has been requested
*/
if (short_irq >= 0 && (wq + tasklet) > 0) {
free_irq(short_irq,NULL);
result = request_irq(short_irq,
tasklet ? short_tl_interrupt :
short_wq_interrupt,
SA_INTERRUPT,"short-bh", NULL);
if (result) {
printk(KERN_INFO "short-bh: can't get assigned irq %i\n",
short_irq);
short_irq = -1;
}
}
return 0;
}
void short_cleanup(void)
{
if (short_irq >= 0) {
outb(0x0, short_base + 2); /* disable the interrupt */
if (!share) free_irq(short_irq, NULL);
else free_irq(short_irq, short_sh_interrupt);
}
/* Make sure we don't leave work queue/tasklet functions running */
if (tasklet)
tasklet_disable(&short_tasklet);
else
flush_scheduled_work();
unregister_chrdev(major, "short");
if (use_mem) {
iounmap((void __iomem *)short_base);
release_mem_region(short_base, SHORT_NR_PORTS);
} else {
release_region(short_base,SHORT_NR_PORTS);
}
if (short_buffer) free_page(short_buffer);
}
module_init(short_init);
module_exit(short_cleanup);
五:使用I/O内存
尽管 I/O 端口在 x86 中很广泛的使用,但在其他平台上,与设备通信的主要方式是通过将设备寄存器和设备内存映射到内存,两者都被称为 I/O内存,因为设备寄存器和设备内存之间的区别对软件来说是透明的。
I/O 内存是设备通过总线提供给处理器访问的一片内存映射(RAM-like)区域, 这种内存可以有多种用途,例如保存视频数据或以太网数据包,以及读写设备寄存器(和读写I/O 端口行为类似)。
访问 I/O 内存的方式取决于所使用的计算机体系结构、总线和设备。根据所使用的计算机平台和总线,I/O 内存可以通过页表访问,也可以不通过页表访问。当通过页表访问时,内核必须首先使物理地址在驱动程序中可见,这通常意味着在进行任何 I/O 操作前必须调用 ioremap。如果不需要页表,I/O 内存看起来就很像 I/O 端口,你只需使用适当的封装函数对其进行读写即可。
无论访问 I/O 内存是否需要 ioremap,都不鼓励直接使用指向 I/O 内存的指针。尽管I/O 内存在硬件层面的寻址方式与普通 RAM 无异,但建议避免使用普通指针。
1. I/O内存的分配和映射
I/O 内存区域必须在使用前分配。分配内存区域的接口(在 <linux/ioport.h> 中定义)为:
struct resource *request_mem_region(unsigned long start, unsigned long len,
char *name);
该函数从起始位置开始,分配一个 len 字节的内存区域。如果一切顺利,则返回一个非空指针;否则返回值为 NULL。所有 I/O 内存分配都会在 /proc/iomem 显示。
当不再使用内存区域时,应释放内存区域,释放函数为:
void release_mem_region(unsigned long start, unsigned long len);
此外,还有一个用于检查 I/O 内存区域可用性的函数,但是这个函数不安全,应避免使用:
int check_mem_region(unsigned long start, unsigned long len);
除了分配 I/O 内存,还必须确保内核可以访问 I/O 内存。因此,必须首先使用ioremap 函数建立映射,该函数专门用于为 I/O 内存区域分配虚拟地址。
一旦使用 ioremap(以及 iounmap),设备驱动程序就可以访问任何 I/O 内存地址,无论它是否直接映射到虚拟地址空间。不过请记住,ioremap 返回的地址不能像普通指针那样使用;相反,应该使用内核提供的访问函数,接下来我们介绍下I/O内存的访问细节。
2. I/O内存访问
在某些平台上,可以将 ioremap 的返回值当作普通指针使用。但这种使用方式并不可移植,而且内核开发人员已经在消除这种使用方式。访问I/O内存的正确方法是通过内核提供一组函数,这些函数有:
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
在这里,addr 是从 ioremap 中获取的地址(可以带有一个整数偏移量);返回值是从给定的 I/O 内存中读取的内容。
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
在这里,addr 是从 ioremap 中获取的地址(可以带有一个整数偏移量);value是要写入的值。
如果必须向给定的 I/O 内存地址读取或写入一系列数值,可以使用函数的读写版本:
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr, const void *buf, unsigned long count);
void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);
这些函数从给定的 buf 向给定的 addr 读取或写入count 个值。
如果阅读内核源代码,你会发现在使用 I/O 内存时,除前面函数之外,还有其他几个I/O内存访问函数被调用。这些函数仍然有效,但不鼓励在新代码中使用。这些函数的安全性较低,因为它们没有执行同样的类型检查。尽管如此,我们还是要在这里介绍一下它们:
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
这些函数用于从 I/O 内存中读取 8 位、16 位和 32 位数据值。
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
与前面的函数一样,这些函数用于写入 8 位、16 位和 32 位数据值。
3. I/O端口映射为I/O内存 (ioport_map)
有些硬件有一个有趣的特点:有些版本使用 I/O 端口,而另一些则使用 I/O 内存。在这两种情况下,输出到处理器的寄存器是相同的,但访问方法却不同。为了方便驱动程序处理这类硬件,同时也为了尽量减少 I/O 端口和内存访问之间的明显差异,2.6 版内核提供了一个名为 ioport_map 的函数:
void *ioport_map(unsigned long port, unsigned int count);
这个函数的作用是将I/O端口转为I/O内存,这样驱动程序就可以在返回的地址上使用 ioread8 和其他函数了。
当不再需要时,应撤销该映射:
void ioport_unmap(void *addr);
这些函数使 I/O 端口看起来像内存。但请注意,I/O 端口在以这种方式重新映射之前,仍必须使用 request_region 进行分配。