原创 I/O 端口和 I/O 内存 4

2014-9-4 09:45 1274 11 11 分类: MCU/ 嵌入式

 

2.5、暂停式I/O操作函数
由于处理器的速率可能与外设(尤其是低速设备)的并不匹配,当处理器过快地传送数据到或自总线时,这时可能就会引起问题。解决方法是:如果在I/O 指令后面紧跟着另一个相似的I/O 指令,就必须插入一个小的延时。为此,Linux提供了暂停式I/O操作函数,这些函数的名子只是在非暂停式I/O操作函数(前面提到的那些I/O操作函数都是非暂停式的)名后加上_p ,如inb_p、outb_p等。大部分体系都支持这些函数,尽管它们常常被扩展为与非暂停 I/O 同样的代码,因为如果体系使用一个合理的现代外设总线,没有必要额外暂停。
以下是ARM体系暂停式I/O宏的定义:
#define outb_p(val,port)    outb((val),(port))
#define outw_p(val,port)    outw((val),(port))
#define outl_p(val,port)    outl((val),(port))
#define inb_p(port)        inb((port))
#define inw_p(port)        inw((port))
#define inl_p(port)        inl((port))
 
#define outsb_p(port,from,len)    outsb(port,from,len)
#define outsw_p(port,from,len)    outsw(port,from,len)
#define outsl_p(port,from,len)    outsl(port,from,len)
#define insb_p(port,to,len)    insb(port,to,len)
#define insw_p(port,to,len)    insw(port,to,len)
#define insl_p(port,to,len)    insl(port,to,len)
 
因为ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。
2.6、平台依赖性
由于自身的特性,I/O指令高度依赖于处理器,非常难以隐藏各体系间的不同。因此,大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:
IA-32 (x86)
x86_64
这个体系支持本章介绍的所有函数;port参数的类型为unsigned short。
ARM
端口映射到内存,并且支持本章介绍的所有函数;port参数的类型为unsigned int;字串函数用C语言实现。
 
3、使用 I/O 内存
尽管 I/O 端口在x86世界中非常流行,但是用来和设备通讯的主要机制是通过内存映射的寄存器和设备内存,两者都称为I/O 内存,因为寄存器和内存之间的区别对软件是透明的。
I/O 内存仅仅是一个类似于RAM 的区域,处理器通过总线访问该区域,以实现对设备的访问。同样,读写这个区域是有边际效应。
根据计算机体系和总线不同,I/O 内存可分为可以或者不可以通过页表来存取。若通过页表存取,内核必须先重新编排物理地址,使其对驱动程序可见,这就意味着在进行任何I/O操作之前,你必须调用ioremap;如果不需要页表,I/O内存区域就类似于I/O端口,你可以直接使用适当的I/O函数读写它们。
由于边际效应的缘故,不管是否需要 ioremap,都不鼓励直接使用I/O内存指针,而应使用专门的I/O内存操作函数。这些I/O内存操作函数不仅在所有平台上是安全,而且对直接使用指针操作 I/O 内存的情况进行了优化。
  3.1、I/O 内存分配和映射
I/O 内存区在使用前必须先分配。分配内存区的函数接口在<linux/ioport.h>定义中:
 
/* request_mem_region分配一个开始于start,len字节的I/O内存区。分配成功,返回一个非NULL指针;否则返回NULL。系统当前所有I/O内存分配信息都在/proc/iomem文件中列出,你分配失败时,可以看看该文件,看谁先占用了该内存区 */
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
 
/* release_mem_region用于释放不再需要的I/O内存区 */
void release_mem_region(unsigned long start, unsigned long len); 
 
/* check_mem_region用于检查I/O内存区的可用性。同样,该函数不安全,不推荐使用 */
int check_mem_region(unsigned long start, unsigned long len);
 
在访问I/O内存之前,分配I/O内存并不是唯一要求的步骤,你还必须保证内核可存取该I/O内存。访问I/O内存并不只是简单解引用指针,在许多体系中,I/O 内存无法以这种方式直接存取。因此,还必须通过ioremap 函数(第8章第4节介绍过)设置一个映射。
#include <asm/io.h>
/* ioremap用于将I/O内存区映射到虚拟地址。参数phys_addr为要映射的I/O内存起始地址,参数size为要映射的I/O内存的大小,返回值为被映射到的虚拟地址 */
void *ioremap(unsigned long phys_addr, unsigned long size);
/* ioremap_nocache为ioremap的无缓存版本。实际上,在大部分体系中,ioremap与ioremap_nocache的实现一样的,因为所有 I/O 内存都是在无缓存的内存地址空间中 */
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
 
/* iounmap用于释放不再需要的映射 */
void iounmap(void * addr);
 
经过 ioremap (和iounmap)之后,设备驱动就可以存取任何I/O内存地址。注意,ioremap返回的地址不可以直接解引用;相反,应当使用内核提供的访问函数。
3.2、访问I/O内存
访问I/O内存的正确方式是通过一系列专门用于实现此目的的函数:
#include <asm/io.h>
/* I/O内存读函数。参数addr应当是从ioremap获得的地址(可能包含一个整型偏移); 返回值是从给定I/O内存读取到的值 */
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
 
/* I/O内存写函数。参数addr同I/O内存读函数,参数value为要写的值 */
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
 
/* 以下这些函数读和写一系列值到一个给定的 I/O 内存地址,从给定的buf读或写count个值到给定的addr。参数count表示要读写的数据个数,而不是字节大小 */
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,,onst void *buf,,nsigned long count);
 
/* 需要操作一块I/O 地址时,使用下列函数(这些函数的行为类似于它们的C库类似函数): */
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);
 
/* 旧的I/O内存读写函数,不推荐使用 */
unsigned readb(address);
unsigned readw(address);
unsigned readl(address); 
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address); 
 
3.3、像I/O 内存一样使用端口
一些硬件有一个有趣的特性: 有些版本使用 I/O 端口;而有些版本则使用 I/O 内存。不管是I/O 端口还是I/O 内存,处理器见到的设备寄存器都是相同的,只是访问方法不同。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:
/* ioport_map重新映射count个I/O端口,使它们看起来I/O内存。此后,驱动程序可以在ioport_map返回的地址上使用ioread8和同类函数。这样,就可以在编程时,消除了I/O 端口和I/O 内存的区别 */
void *ioport_map(unsigned long port, unsigned int count); 
 
/* ioport_unmap用于释放不再需要的映射 */
void ioport_unmap(void *addr);
 
注意,I/O 端口在重新映射前必须使用request_region分配分配所需的I/O 端口。
4、ARM体系的I/O操作接口
s3c24x0处理器使用的是I/O内存,也就是说:s3c24x0处理器使用统一编址方式,I/O寄存器和内存使用的是单一地址空间,并且读写I/O寄存器和读写内存的指令是相同的。所以推荐使用I/O内存的相关指令和函数。但这并不表示I/O端口的指令在s3c24x0中不可用。如果你注意过s3c24x0关于I/O方面的内核源码,你就会发现:其实I/O端口的指令只是一个外壳,内部还是使用和I/O内存一样的代码。
下面是ARM体系原始的I/O操作函数。其实后面I/O端口和I/O内存操作函数,只是对这些函数进行再封装。从这里也可以看出为什么我们不推荐直接使用I/O端口和I/O内存地址指针,而是要求使用专门的I/O操作函数——专门的I/O操作函数会检查地址指针是否有效是否为IO地址(通过__iomem或__chk_io_ptr)
 
#include <asm-arm/io.h>
/*
 * Generic IO read/write. These perform native-endian accesses. Note
 * that some architectures will want to re-define __raw_{read,write}w.
 */
extern void __raw_writesb(void __iomem *addr, const void *data, int bytelen);
extern void __raw_writesw(void __iomem *addr, const void *data, int wordlen);
extern void __raw_writesl(void __iomem *addr, const void *data, int longlen);
 
extern void __raw_readsb(const void __iomem *addr, void *data, int bytelen);
extern void __raw_readsw(const void __iomem *addr, void *data, int wordlen);
extern void __raw_readsl(const void __iomem *addr, void *data, int longlen);
 
#define __raw_writeb(v,a)    (__chk_io_ptr(a), *(volatile unsigned char __force *)(a) =(v))
#define __raw_writew(v,a)    (__chk_io_ptr(a), *(volatile unsigned short __force *)(a) =(v))
#define __raw_writel(v,a)    (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) =(v))
 
#define __raw_readb(a)        (__chk_io_ptr(a), *(volatile unsigned char __force *)(a))
#define __raw_readw(a)        (__chk_io_ptr(a), *(volatile unsigned short __force *)(a))
#define __raw_readl(a)        (__chk_io_ptr(a), *(volatile unsigned int __force *)(a))
 
关于__force和__iomem
 
#include <linux/compiler.h>
/* __force表示所定义的变量类型是可以做强制类型转换的 */
#define __force __attribute__((force)) 
 
/* __iomem是用来修饰一个变量的,这个变量必须是非解引用(no dereference)的,即这个变量地址必须是有效的,而且变量所在的地址空间必须是2,即设备地址映射空间。0表示normal space,即普通地址空间,对内核代码来说,当然就是内核空间地址了。1表示用户地址空间,2表示是设备地址映射空间 */
#define __iomem __attribute__((noderef, address_space(2)))
 
I/O端口
 
#include <asm-arm/io.h>
#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; })
 
#define outsb(p,d,l)        __raw_writesb(__io(p),d,l)
#define outsw(p,d,l)        __raw_writesw(__io(p),d,l)
#define outsl(p,d,l)        __raw_writesl(__io(p),d,l)
 
#define insb(p,d,l)        __raw_readsb(__io(p),d,l)
#define insw(p,d,l)        __raw_readsw(__io(p),d,l)
#define insl(p,d,l)        __raw_readsl(__io(p),d,l)
 
I/O内存
 
#include <asm-arm/io.h>
#define ioread8(p)    ({ unsigned int __v = __raw_readb(p); __v; })
#define ioread16(p)    ({ unsigned int __v = le16_to_cpu((__force __le16)__raw_readw(p));__v; })
#define ioread32(p)    ({ unsigned int __v = le32_to_cpu((__force __le32)__raw_readl(p));__v; })
 
#define iowrite8(v,p)    __raw_writeb(v, p)
#define iowrite16(v,p)    __raw_writew((__force __u16)cpu_to_le16(v), p)
#define iowrite32(v,p)    __raw_writel((__force __u32)cpu_to_le32(v), p)
 
#define ioread8_rep(p,d,c)    __raw_readsb(p,d,c)
#define ioread16_rep(p,d,c)    __raw_readsw(p,d,c)
#define ioread32_rep(p,d,c)    __raw_readsl(p,d,c)
 
#define iowrite8_rep(p,s,c)    __raw_writesb(p,s,c)
#define iowrite16_rep(p,s,c)    __raw_writesw(p,s,c)
#define iowrite32_rep(p,s,c)    __raw_writesl(p,s,c)
 
注意:
1)、所有的读写指令(I/O操作函数)所赋的地址必须都是虚拟地址,你有两种选择:使用内核已经定义好的地址,如在include/asm-arm/arch-s3c2410/regs-xxx.h中定义了s3c2410处理器各外设寄存器地址(其他处理器芯片也可在类似路径找到内核定义好的外设寄存器的虚拟地址;另一种方法就是使用自己用ioremap映射的虚拟地址。绝对不能使用实际的物理地址,否则会因为内核无法处理地址而出现oops。
2)、在使用I/O指令时,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因为request的功能只是告诉内核端口被谁占用了,如再次request,内核会制止(资源busy)。但是不推荐这么做,这样的代码也不规范,可能会引起并发问题(很多时候我们都需要独占设备)。
3)、在使用I/O指令时,所赋的地址数据有时必须通过强制类型转换为 unsigned long ,不然会有警告。
4)、在include\asm-arm\arch-s3c2410\hardware.h中定义了很多io口的操作函数,有需要可以在驱动中直接使用,很方便。
ns in ad a c

文章评论0条评论)

登录后参与讨论
我要评论
0
11
关闭 站长推荐上一条 /2 下一条