Linux设备驱动程序--与硬件通信(1)(3)
平台相关性
由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和arm所使用函数的总结:
IA-32 (x86)
x86_64
这个体系支持所有的以上描述的函数,端口号是 unsigned short 类型。
arm
端口映射到内存,支持所有函数。串操作 用C语言实现。端口是 unsigned int 类型。
使用 I/O 内存
除了 x86上普遍使用的I/O
端口外,和设备通讯另一种主要机制是通过使用映射到内存的寄存器或设备内存,统称为 I/O 内存。因为寄存器和内存之间的区别对软件是透明的。I/O
内存仅仅是类似 RAM 的一个区域,处理器通过总线访问这个区域,以实现设备的访问。
根据平台和总线的不同,I/O
内存可以就是否通过页表访问分类。若通过页表访问,内核必须首先安排物理地址使其对设备驱动程序可见,在进行任何 I/O 之前必须调用
ioremap。若不通过页表,I/O 内存区域就类似I/O 端口,可以使用适当形式的函数访问它们。因为“side effect”的影响,不管是否需要 ioremap ,都不鼓励直接使用 I/O 内存的指针。而使用专用的 I/O 内存操作函数,不仅在所有平台上是安全,而且对直接使用指针操作 I/O 内存的情况进行了优化。
I/O 内存分配和映射
I/O 内存区域使用前必须先分配,函数接口在 定义:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);/* 从 start 开始,分配一个 len 字节的内存区域。成功返回一个非NULL指针,否则返回NULL。所有的 I/O 内存分配情况都 /proc/iomem 中列出。*/
/*I/O内存区域在不再需要时应当释放*/
void release_mem_region(unsigned long start, unsigned long len);
/*一个旧的检查 I/O 内存区可用性的函数,不推荐使用*/
int check_mem_region(unsigned long start, unsigned long len);
然后必须设置一个映射,由 ioremap 函数实现,此函数专门用来为I/O
内存区域分配虚拟地址。经过ioremap 之后,设备驱动即可访问任意的 I/O 内存地址。注意:ioremap
返回的地址不应当直接引用;应使用内核提供的 accessor 函数。以下为函数定义:
#include asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);/*如果控制寄存器也在该区域,应使用的非缓存版本,以实现side effect。*/
void iounmap(void * addr);
访问I/O 内存
访问I/O 内存的正确方式是通过一系列专用于此目的的函数(在 中定义的):
/*I/O 内存读函数*/
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
/*addr 是从 ioremap 获得的地址(可能包含一个整型偏移量), 返回值是从给定 I/O 内存读取的值*/
/*对应的I/O 内存写函数*/
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
/*读和写一系列值到一个给定的 I/O 内存地址,从给定的 buf 读或写 count 个值到给定的 addr */
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);
/*需要操作一块 I/O 地址,使用一下函数*/
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);
/*旧函数接口,仍可工作, 但不推荐。*/
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
像 I/O 内存一样使用端口
一些硬件有一个有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 内存。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:
void *ioport_map(unsigned long port, unsigned int count);/*重映射 count 个I/O 端口,使其看起来像 I/O 内存。,此后,驱动程序可以在返回的地址上使用 ioread8 和同类函数。其在编程时消除了I/O 端口和I/O 内存的区别。
/*这个映射应当在它不再被使用时撤销:*/
void ioport_unmap(void *addr);
/*注意:I/O 端口仍然必须在重映射前使用 request_region 分配I/O 端口。arm9不支持这两个函数!*/
上面是基于《Linux设备驱动程序第3版)》的介绍,以下分析 arm9的s3c2440A的linux驱动接口。
arm9的linux驱动接口
s3c24x0处理器是使用I/O内存的,也就是说:他们的外设接口是通过读写相应的寄存器实现的,这些寄存器和内存是使用单一的地址空间,并使用和读写内存一样的指令。所以推荐使用I/O内存的相关指令。
但这并不表示I/O端口的指令在s3c24x0中不可用。但是只要你注意其源码,你就会发现:其实I/O端口的指令只是一个外壳,内部还是使用和I/O内存一样的代码。以下列出一些:
I/O端口
#define outb(v,p) __raw_writeb(v,__io(p))
#define outw(v,p) __raw_writew((__force __u16) \
cpu_to_le16(v),__io(p))
#define outl(v,p) __raw_writel((__force __u32) \
cpu_to_le32(v),__io(p))
#define inb(p) ({ __u8 __v = __raw_readb(__io(p)); __v; })
#define inw(p) ({ __u16 __v = le16_to_cpu((__force __le16) \
__raw_readw(__io(p))); __v; })
#define inl(p) ({ __u32 __v = le32_to_cpu((__force __le32) \
__raw_readl(__io(p))); __v; })
I/O内存
#define ioread8(p) ({ unsigned int __v = __raw_readb(p); __v; })
#define ioread16(p) ({ unsigned int __v = le16_to_cpu(__raw_readw(p)); __v; })
#define ioread32(p) ({ unsigned int __v = le32_to_cpu(__raw_readl(p)); __v; })
#define iowrite8(v,p) __raw_writeb(v, p)
#define iowrite16(v,p) __raw_writew(cpu_to_le16(v), p)
#define iowrite32(v,p) __raw_writel(cpu_to_le32(v), p)
我对I/O端口的指令和I/O内存的指令都写了相应的驱动程序,都通过了测试。在这里值得注意的有4点:
1)所有的读写指令所赋的地址必须都是虚拟地址,你有两种选择:使用内核已经定
义好的地址,如
S3C2440_GPJCON等等,这些都是内核定义好的虚拟地址,有兴趣的可以看源码。还有一种方法就是使用自己用ioremap映射的虚拟地址。绝对
不能使用实际的物理地址,否则会因为内核无法处理地址而出现oops。
2)在使用I/O指令时,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因为request的功能只是告诉内核端口被谁占用了,如再次request,内核会制止。
3)在使用I/O指令时,所赋的地址数据有时必须通过强制类型转换为 unsigned long ,不然会有警告(具体原因请看
Linux设备驱动程序学习7)-内核的数据类型
) 。虽然你的程序可能也可以使用,但是最好还是不要有警告为妙。
4)在include\asm-arm\arch-s3c2410\hardware.h中定义了很多io口的操作函数,有需要可以在驱动中直接使用,很方便。
实验源码:
IO_port.tar.gz
IO_port_test.tar.gz
IO_mem.tar.gz
IO_mem_test.tar.gz
两个模块都实现了阻塞型独享设备的访问控制,并通知内核不支持llseek。具体的测试在IO_port中。
测试现象如下:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod IO_port.ko
[Tekkaman2440@SBC2440V4]#insmod IO_mem.ko
[Tekkaman2440@SBC2440V4]#cat /proc/devices
Character devices:
1 mem
2 pty
3 ttyp
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
10 misc
13 input
14 sound
81 video4linux
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
153 spi
180 usb
189 usb_device
204 s3c2410_serial
251 IO_mem
252 IO_port
253 usb_endpoint
254 rtc
Block devices:
1 ramdisk
评论暂时关闭