精华内容
下载资源
问答
  • 大家知道,STC单片机 IO端口四个模式:准双向(默认)、推挽、高阻、开漏。通过PxM1/PxM0 2个寄存器 进行设置。这个小工具STCIOset V1.0b9h 可以很方便地生成设置代码,而且还有C51的例程。(原创)
  • 实现数码管、按键、流水灯控制设计功能。具体任务要求描述:1.初始化,四个数码管灭,4个LED灯灭,...5.第四个按键按下后第四个数码管显示4,同时第四个LED灯亮。</p>
  • IO端口和IO内存的区别

    2019-08-20 18:17:56
    、外设IO端口物理地址的编址方式 统一编址 独立编址 优缺点 五、Linux下访问IO端口 I/O映射方式 内存映射方式 六、Linux下访问IO内存 六、ioremap和ioport_map 七、总结 IO端...

     

    1. Linux系统对IO端口和IO内存的管理
      1. 一、I/O端口
      2. 二、IO内存
      3. 三、IO端口和IO内存的区分及联系
      4. 四、外设IO端口物理地址的编址方式
        1. 统一编址
        2. 独立编址
        3. 优缺点
      5. 五、Linux下访问IO端口
        1. I/O映射方式
        2. 内存映射方式
      6. 六、Linux下访问IO内存
      7. 六、ioremap和ioport_map
      8. 七、总结

    IO端口和IO内存的区别及分别使用的函数接口 

             每个外设都是通过读写其寄存器来控制的。外设寄存器也称为I/O端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。根据访问外设寄存器的不同方式,可以把CPU分成两大类。一类CPU(如M68K,Power PC等)把这些寄存器看作内存的一部分,寄存器参与内存统一编址,访问寄存器就通过访问一般的内存指令进行,所以,这种CPU没有专门用于设备I/O的指令。这就是所谓的“I/O内存”方式。另一类CPU(典型的如X86),将外设的寄存器看成一个独立的地址空间,所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令。这就是所谓的“ I/O端口”方式。但是,用于I/O指令的“地址空间”相对来说是很小的,如x86 CPU的I/O空间就只有64KB(0-0xffff)。

            结合下图,我们彻底讲述IO端口和IO内存以及内存之间的关系。主存16M字节的SDRAM,外设是个视频采集卡,上面有16M字节的SDRAM作为缓冲区。

     

    1.   CPU是i386架构的情况

     在i386系列的处理中,内存和外部IO是独立编址,也是独立寻址的。MEM的内存空间是32位可以寻址到4G,IO空间是16位可以寻址到64K。

     在Linux内核中,访问外设上的IO Port必须通过IO Port的寻址方式。而访问IO Mem就比较罗嗦,外部MEM不能和主存一样访问,虽然大小上不相上下,可是外部MEM是没有在系统中注册的。访问外部IO MEM必须通过remap映射到内核的MEM空间后才能访问。为了达到接口的同一性,内核提供了IO Port到IO Mem的映射函数。映射后IO Port就可以看作是IO Mem,按照IO Mem的访问方式即可。

    3.    CPU是ARM或PPC架构的情况

    在这一类的嵌入式处理器中,IO Port的寻址方式是采用内存映射,也就是IO bus就是Mem bus。系统的寻址能力如果是32位,IO Port+Mem(包括IO Mem)可以达到4G。

          

    1.使用I/O 端口

    I/O 端口是驱动用来和很多设备通讯的方法。

    1.1、分配I/O 端口

    在驱动还没独占设备之前,不应对端口进行操作。内核提供了一个注册接口,以允许驱动声明其需要的端口:

    #include <linux/ioport.h>
    /* request_region告诉内核:要使用first开始的n个端口。参数name为设备名。如果分配成功返回值是非NULL;否则无法使用需要的端 口(/proc/ioports包含了系统当前所有端口的分配信息,若request_region分配失败时,可以查看该文件,看谁先用了你要的端口) */
    struct resource *request_region(unsigned long first, unsigned long n, const char *name);
    /* 用完I/O端口后(可能在模块卸载时),应当调用release_region将I/O端口返还给系统。参数start和n应与之前传递给request_region一致 */
    void release_region(unsigned long start, unsigned long n); 
    /* check_region用于检查一个给定的I/O端 口集是否可用。如果给定的端口不可用,check_region返回一个错误码。不推荐使用该函数,因为即便它返回0(端口可用),它也不能保证后面的端 口分配操作会成功,因为检查和后面的端口分配并不是一个原子操作。而request_region通过加锁来保证操作的原子性,因此是安全的 */
    int check_region(unsigned long first, unsigned long n);


          1.2、操作I/O端口

    在驱动成功请求到I/O 端口后,就可以读写这些端口了。大部分硬件会将8位、16位和32位端口区分开,无法像访问内存那样混淆使用。驱动程序必须调用不同的函数来访问不同大小的端口。

    如同前面所讲的,仅支持单地址空间的计算机体系通过将I/O端口地址重新映射到内存地址来伪装端口I/O 。为了提高移植性,内核对驱动隐藏了这些细节。Linux 内核头文件(体系依赖的头文件<asm/io.h>) 定义了下列内联函数来存取I/O端口:

    /* inb/outb:读/写字节端口(8位宽)。有些体系将port参数定义为unsigned long;而有些平台则将它定义为unsigned short。inb的返回类型也是依赖体系的 */
    unsigned inb(unsigned port);
    void outb(unsigned char byte, unsigned port);
    /* inw/outw:读/写字端口(16位宽) */
    unsigned inw(unsigned port);
    void outw(unsigned short word, unsigned port);
    /* inl/outl:读/写32位端口。longword也是依赖体系的,有的体系为unsigned long;而有的为unsigned int */
    unsigned inl(unsigned port);
    void outl(unsigned longword, unsigned port);

    从现在开始,当我们使用unsigned 没有进一步指定类型时,表示是一个依赖体系的定义。

    注意,没有64位的I/O端口操作函数。即便在64位体系中,端口地址空间使用一个32位(最大)的数据通路。

    1.3、从用户空间访问I/O端口

    1.2节介绍的函数主要是提供给驱动使用,但它们也可在用户空间使用,至少在PC机上可以。GNU C 库在 <sys/io.h> 中定义它们。如果在用户空间使用这些函数,必须满足下列条件:

    1)、程序必须使用-O选项编译来强制扩展内联函数

    2)、必须使用ioperm和iopl系统调用(#include <sys/perm.h>) 来获得进行操作I/O端口的权限。ioperm 为获取单个端口的操作许可,iopl 为获取整个I/O空间许可。这2个函数都是x86特有的

    3)、程序必须以root来调用ioperm或者iopl,或者其父进程(祖先)必须以root获得的端口操作权限

    如果平台不支持ioperm和iopl系统调用,通过使用/dev/prot设备文件,用户空间仍然可以存取I/O 端口。但是要注意的是,这个文件的定义也是依赖平台的。

    1.4、字串操作

    除了一次传递一个数据的I/O操作,某些处理器实现了一次传递一序列数据(单位可以是字节、字和双字)的特殊指令。这些所谓的字串指令,它们完成任务比一个C语言循环更快。下列宏定义实现字串操作,在某些体系上,它们通过使用单个机器指令实现;但如果目标处理器没有进行字串I/O指令,则通过执行一个紧凑的循环实现。

    字串函数的原型是:

    /* insb:从I/O端口port读取count个数据(单位字节)到以内存地址addr为开始的内存空间 */
    void insb(unsigned port, void *addr, unsigned long count);
    /* outsb:将内存地址addr开始的count个数据(单位字节)写到I/O端口port */
    void outsb(unsigned port, void *addr, unsigned long count);
    /* insw:从I/O端口port读取count个数据(单位字)到以内存地址addr为开始的内存空间 */
    void insw(unsigned port, void *addr, unsigned long count);
    /* outsw:将内存地址addr开始的count个数据(单位字)写到I/O端口port */
    void outsw(unsigned port, void *addr, unsigned long count);
    /* insl:从I/O端口port读取count个数据(单位双字)到以内存地址addr为开始的内存空间 */
    void insl(unsigned port, void *addr, unsigned long count);
    /* outsl:将内存地址addr开始的count个数据(单位双字)写到I/O端口port */
    void outsl(unsigned port, void *addr, unsigned long count);

    注意:使用字串函数时,它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。使用 inw读取端口应在必要时自行转换字节序,以匹配主机字节序。

    1.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同样的代码。

    1.6、平台依赖性

    由于自身的特性,I/O指令高度依赖于处理器,非常难以隐藏各体系间的不同。因此,大部分的关于端口 I/O的源码是平台依赖的。以下是x86和ARM所使用函数的总结:

    IA-32 (x86)

    x86_64

    这个体系支持本章介绍的所有函数;port参数的类型为unsigned short。

    ARM

    端口映射到内存,并且支持本章介绍的所有函数;port参数的类型为unsigned int;字串函数用C语言实现。

     

           2、使用 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 内存的情况进行了优化。

    2.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 函数设置一个映射。

    #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返回的地址不可以直接解引用;相反,应当使用内核提供的访问函数。

    2.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); 

    2.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 端口。


           3、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口的操作函数,有需要可以在驱动中直接使用,很方便。

     

     

     

     

     

     

    Linux系统对IO端口和IO内存的管理

    http://blog.csdn.net/ce123/article/details/7204458

    一、I/O端口

          端口(port)是接口电路中能被CPU直接访问的寄存器的地址。几乎每一种外设都是通过读写设备上的寄存器来进行的。CPU通过这些地址即端口向接口电 路中的寄存器发送命令,读取状态和传送数据。外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存 器通常被连续地编址。

    二、IO内存

           例如,在PC上可以插上一块图形卡,有2MB的存储空间,甚至可能还带有ROM,其中装有可执行代码。

     

    三、IO端口和IO内存的区分及联系

             这两者如何区分就涉及到硬件知识,X86体系中,具有两个地址空间:IO空间和内存空间,而RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,即内存空间。
    内存空间:内存地址寻址范围,32位操作系统内存空间为2的32次幂,即4G。
    IO空间:X86特有的一个空间,与内存空间彼此独立的地址空间,32位X86有64K的IO空间。

    IO端口:当寄存器或内存位于IO空间时,称为IO端口。一般寄存器也俗称I/O端口,或者说I/O ports,这个I/O端口可以被映射在Memory Space,也可以被映射在I/O Space。

    IO内存:当寄存器或内存位于内存空间时,称为IO内存。

     

    四、外设IO端口物理地址的编址方式

            CPU对外设IO端口物理地址的编址方式有两种:一种是I/O映射方式(I/O-mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于CPU的体系结构。

    1、统一编址

      RISC指令系统的CPU(如,PowerPC、m68k、ARM等)通常只实现一个物 理地址空间(RAM)。在这种情况下,外设I/O端口的物理地址就被映射到CPU的单一物理地址空间中,而成为内存的一部分。此时,CPU可以象访问一个 内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。

           统一编址也称为“I/O内存”方式,外设寄存器位于“内存空间”(很多外设有自己的内存、缓冲区,外设的寄存器和内存统称“I/O空间”)。

    2、独立编址

            而另外一些体系结构的CPU(典型地如X86)则为外设专门实现了一个单独地地址空间,称为“I/O地址空间”或者“I/O端口空间”。这是一个与CPU 地RAM物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专门的I/O指令(如X86的IN和OUT指令)来访 问这一空间中的地址单元(也即I/O端口)。与RAM物理地址空间相比,I/O地址空间通常都比较小,如x86 CPU的I/O空间就只有64KB(0-0xffff)。这是“I/O映射方式”的一个主要缺点。

            独立编址也称为“I/O端口”方式,外设寄存器位于“I/O(地址)空间”。

    3、优缺点

    独立编址主要优点是:
    1)、I/O端口地址不占用存储器空间;使用专门的I/O指令对端口进行操作,I/O指令短,执行速度快。
    2)、并且由于专门I/O指令与存储器访问指令有明显的区别,使程序中I/O操作和存储器操作层次清晰,程序的可读性强。
    3)、同时,由于使用专门的I/O指令访问端口,并且I/O端口地址和存储器地址是分开的,故I/O端口地址和存储器地址可以重叠,而不会相互混淆。 
    4)、译码电路比较简单(因为I/0端口的地址空间一般较小,所用地址线也就较少)。
    其缺点是:只能用专门的I/0指令,访问端口的方法不如访问存储器的方法多。

    统一编址优点:
    1)、由于对I/O设备的访问是使用访问存储器的指令,所以指令类型多,功能齐全,这不仅使访问I/O端口可实现输入/输出操作,而且还可对端口内容进行算术逻辑运算,移位等等;
    2)、另外,能给端口有较大的编址空间,这对大型控制系统和数据通信系统是很有意义的。
    这种方式的缺点是端口占用了存储器的地址空间,使存储器容量减小,另外指令长度比专门I/O指令要长,因而执行速度较慢。
            究竟采用哪一种取决于系统的总体设计。在一个系统中也可以同时使用两种方式,前提是首先要支持I/O独立编址。Intel的x86微处理器都支持I/O 独立编址,因为它们的指令系统中都有I/O指令,并设置了可以区分I/O访问和存储器访问的控制信号引脚。而一些微处理器或单片机,为了减少引脚,从而减 少芯片占用面积,不支持I/O独立编址,只能采用存储器统一编址。

    五、Linux下访问IO端口

               对于某一既定的系统,它要么是独立编址、要么是统一编址,具体采用哪一种则取决于CPU的体系结构。 如,PowerPC、m68k等采用统一编址,而X86等则采用独立编址,存在IO空间的概念。目前,大多数嵌入式微控制器如ARM、PowerPC等并 不提供I/O空间,仅有内存空间,可直接用地址、指针访问。但对于Linux内核而言,它可能用于不同的CPU,所以它必须都要考虑这两种方式,于是它采 用一种新的方法,将基于I/O映射方式的或内存映射方式的I/O端口通称为“I/O区域”(I/O region),不论你采用哪种方式,都要先申请IO区域:request_resource(),结束时释放 它:release_resource()。

    IO region是一种IO资源,因此它可以用resource结构类型来描述。

             访问IO端口有2种途径:I/O映射方式(I/O-mapped)、内存映射方式(Memory-mapped)。前一种途径不映射到内存空间,直接使用 intb()/outb()之类的函数来读写IO端口;后一种MMIO是先把IO端口映射到IO内存(“内存空间”),再使用访问IO内存的函数来访问 IO端口。

    1、I/O映射方式

           直接使用IO端口操作函数:在设备打开或驱动模块被加载时申请IO端口区域,之后使用inb(),outb()等进行端口访问,最后在设备关闭或驱动被卸载时释放IO端口范围。

     in、out、ins和outs汇编语言指令都可以访问I/O端口。内核中包含了以下辅助函数来简化这种访问:

    inb( )、inw( )、inl( )
    分别从I/O端口读取1、2或4个连续字节。后缀“b”、“w”、“l”分别代表一个字节(8位)、一个字(16位)以及一个长整型(32位)。

    inb_p( )、inw_p( )、inl_p( )
    分别从I/O端口读取1、2或4个连续字节,然后执行一条“哑元(dummy,即空指令)”指令使CPU暂停。

    outb( )、outw( )、outl( )
    分别向一个I/O端口写入1、2或4个连续字节。

    outb_p( )、outw_p( )、outl_p( )
    分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“哑元”指令使CPU暂停。

    insb( )、insw( )、insl( )
    分别从I/O端口读入以1、2或4个字节为一组的连续字节序列。字节序列的长度由该函数的参数给出。

    outsb( )、outsw( )、outsl( )
    分别向I/O端口写入以1、2或4个字节为一组的连续字节序列。

    流程如下:

     

          虽然访问I/O端口非常简单,但是检测哪些I/O端口已经分配给I/O设备可能就不这么简单了,对基于ISA总线的系统来说更是如此。通常,I/O设备驱 动程序为了探测硬件设备,需要盲目地向某一I/O端口写入数据;但是,如果其他硬件设备已经使用这个端口,那么系统就会崩溃。为了防止这种情况的发生,内 核必须使用“资源”来记录分配给每个硬件设备的I/O端口。资源表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。在这里,资源表示I/O端口地 址的一个范围。每个资源对应的信息存放在resource数据结构中:

    [plain] view plaincopy

     

    1. struct resource {  
    2.          resource_size_t start;// 资源范围的开始  
    3.          resource_size_t end;// 资源范围的结束  
    4.          const char *name; //资源拥有者的名字  
    5.          unsigned long flags;// 各种标志  
    6.          struct resource *parent, *sibling, *child;// 指向资源树中父亲,兄弟和孩子的指针  
    7. };  


              所有的同种资源都插入到一个树型数据结构(父亲、兄弟和孩子)中;例如,表示I/O端口地址范围的所有资源都包括在一个根节点为 ioport_resource的树中。节点的孩子被收集在一个链表中,其第一个元素由child指向。sibling字段指向链表中的下一个节点。

            为什么使用树?例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000 到 0xf00f。那么,start字段为0xf000 且end 字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。但是,IDE设备驱动程序需要记住另外的信息,也就是IDE链主盘 使用0xf000 到0xf007的子范围,从盘使用0xf008 到0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000 到0xf00f的整个范围对应的资源下。一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。I/O端口资源树 (ioport_resource)的根节点跨越了整个I/O地址空间(从端口0到65535)。

    任何设备驱动程序都可以使用下面三个函数,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:

    request_resource( )        //把一个给定范围分配给一个I/O设备。

    allocate_resource( )        //在资源树中寻找一个给定大小和排列方式的可用范围;若存在,将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,可以使用任意的端口号和主板上的内存地址对其进行配置)。

    release_resource( )      //释放以前分配给I/O设备的给定范围。

    内核也为以上函数定义了一些应用于I/O端口的快捷函数:request_region( )分配I/O端口的给定范围,release_region( )释放以前分配给I/O端口的范围。当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。

    2、内存映射方式

             将IO端口映射为内存进行访问,在设备打开或驱动模块被加载时,申请IO端口区域并使用ioport_map()映射到内存,之后使用IO内存的函数进行端口访问,最后,在设备关闭或驱动模块被卸载时释放IO端口并释放映射。

    映射函数的原型为:
    void *ioport_map(unsigned long port, unsigned int count);
    通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。但请注意,在进行映射前,还必须通过request_region( )分配I/O端口。

    当不再需要这种映射时,需要调用下面的函数来撤消:
    void ioport_unmap(void *addr);

    在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是宜使用Linux内核的如下一组函数来完成访问I/O内存:·读I/O内存
    unsigned int ioread8(void *addr);
    unsigned int ioread16(void *addr);
    unsigned int ioread32(void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    unsigned readb(address);
    unsigned readw(address);
    unsigned readl(address);
    ·写I/O内存
    void iowrite8(u8 value, void *addr);
    void iowrite16(u16 value, void *addr);
    void iowrite32(u32 value, void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    void writeb(unsigned value, address);
    void writew(unsigned value, address);
    void writel(unsigned value, address);

    流程如下:

     

    六、Linux下访问IO内存

             IO内存的访问方法是:首先调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间的虚拟地址, 之后就可以Linux设备访问编程接口访问这些寄存器了,访问完成后,使用ioremap()对申请的虚拟地址进行释放,并释放 release_mem_region()申请的IO内存资源。

    struct resource *requset_mem_region(unsigned long start, unsigned long len,char *name);
       这个函数从内核申请len个内存地址(在3G~4G之间的虚地址),而这里的start为I/O物理地址,name为设备的名称。注意,。如果分配成功,则返回非NULL,否则,返回NULL。
    另外,可以通过/proc/iomem查看系统给各种设备的内存范围。

    要释放所申请的I/O内存,应当使用release_mem_region()函数:
    void release_mem_region(unsigned long start, unsigned long len)

    申请一组I/O内存后, 调用ioremap()函数:
    void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
    其中三个参数的含义为:
    phys_addr:与requset_mem_region函数中参数start相同的I/O物理地址;
    size:要映射的空间的大小;
    flags:要映射的IO空间的和权限有关的标志;

    功能:将一个I/O地址空间映射到内核的虚拟地址空间上(通过release_mem_region()申请到的)

    流程如下:

     

    六、ioremap和ioport_map

    下面具体看一下ioport_map和ioport_umap的源码:

    [plain] view plaincopy

     

    1. void __iomem *ioport_map(unsigned long port, unsigned int nr)  
    2. {  
    3.     if (port > PIO_MASK)  
    4.         return NULL;  
    5.     return (void __iomem *) (unsigned long) (port + PIO_OFFSET);  
    6. }  
    7.   
    8. void ioport_unmap(void __iomem *addr)  
    9. {  
    10.     /* Nothing to do */  
    11. }  

              ioport_map仅仅是将port加上PIO_OFFSET(64k),而ioport_unmap则什么都不做。这样portio的64k空间就被映射到虚拟地址的64k~128k之间,而ioremap返回的虚拟地址则肯定在3G之上。ioport_map函数的目的是试图提供与ioremap一致的虚拟地址空间。分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口ioread8/iowrite8(......)访问I/O端口。
              最后来看一下ioread8的源码,其实现也就是对虚拟地址进行了判断,以区分IO端口和IO内存,然后分别使用inb/outb和readb/writeb来读写。

    [plain] view plaincopy

     

    1. unsigned int fastcall ioread8(void __iomem *addr)  
    2. {  
    3.     IO_COND(addr, return inb(port), return readb(addr));  
    4. }  
    5.   
    6. #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)  
    7. #define IO_COND(addr, is_pio, is_mmio) do { \  
    8.     unsigned long port = (unsigned long __force)addr; \  
    9.         if (port < PIO_RESERVED) { \  
    10.             VERIFY_PIO(port); \  
    11.             port &= PIO_MASK; \  
    12.             is_pio; \  
    13.         } else { \  
    14.             is_mmio; \  
    15.         } \  
    16. } while (0)  
    17.   
    18. 展开:  
    19. unsigned int fastcall ioread8(void __iomem *addr)  
    20. {  
    21.     unsigned long port = (unsigned long __force)addr;  
    22.     if( port < 0x40000UL ) {  
    23.         BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );  
    24.         port &= PIO_MASK;  
    25.         return inb(port);  
    26.     }else{  
    27.         return readb(addr);  
    28.     }  
    29. }  

    七、总结

             外设IO寄存器地址独立编址的CPU,这时应该称外设IO寄存器为IO端 口,访问IO寄存器可通过ioport_map将其映射到虚拟地址空间,但实际上这是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为 了可以使用和IO内存一样的接口访问IO寄存器;也可以直接使用in/out指令访问IO寄存器。

              例如:Intel x86平台普通使用了名为内存映射(MMIO)的技术,该技术是PCI规范的一部分,IO设备端口被映射到内存空间,映射后,CPU访问IO端口就如同访 问内存一样。

              外设IO寄存器地址统一编址的CPU,这时应该称外设IO寄存器为IO内存,访问IO寄存器可通过ioremap将其映射到虚拟地址空间,然后再使用read/write接口访问。

    展开全文
  • 目录(?)[-] Linux系统对IO端口和IO内存... 、外设IO端口物理地址的编址方式 统一编址 独立编址 优缺点 五、Linux下访问IO端口 I/O映射方式 内存映射方式 ...

    目录(?)[-]

    1. Linux系统对IO端口和IO内存的管理
      1. 一、I/O端口
      2. 二、IO内存
      3. 三、IO端口和IO内存的区分及联系
      4. 四、外设IO端口物理地址的编址方式
        1. 统一编址
        2. 独立编址
        3. 优缺点
      5. 五、Linux下访问IO端口
        1. I/O映射方式
        2. 内存映射方式
      6. 六、Linux下访问IO内存
      7. 六、ioremap和ioport_map
      8. 七、总结

    IO端口和IO内存的区别及分别使用的函数接口 

             每个外设都是通过读写其寄存器来控制的。外设寄存器也称为I/O端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。根据访问外设寄存器的不同方式,可以把CPU分成两大类。一类CPU(如M68K,Power PC等)把这些寄存器看作内存的一部分,寄存器参与内存统一编址,访问寄存器就通过访问一般的内存指令进行,所以,这种CPU没有专门用于设备I/O的指令。这就是所谓的“I/O内存”方式。另一类CPU(典型的如X86),将外设的寄存器看成一个独立的地址空间,所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令。这就是所谓的“ I/O端口”方式。但是,用于I/O指令的“地址空间”相对来说是很小的,如x86 CPU的I/O空间就只有64KB(0-0xffff)。

            结合下图,我们彻底讲述IO端口和IO内存以及内存之间的关系。主存16M字节的SDRAM,外设是个视频采集卡,上面有16M字节的SDRAM作为缓冲区。

     

    1.   CPU是i386架构的情况

     在i386系列的处理中,内存和外部IO是独立编址,也是独立寻址的。MEM的内存空间是32位可以寻址到4G,IO空间是16位可以寻址到64K。

     在Linux内核中,访问外设上的IO Port必须通过IO Port的寻址方式。而访问IO Mem就比较罗嗦,外部MEM不能和主存一样访问,虽然大小上不相上下,可是外部MEM是没有在系统中注册的。访问外部IO MEM必须通过remap映射到内核的MEM空间后才能访问。为了达到接口的同一性,内核提供了IO Port到IO Mem的映射函数。映射后IO Port就可以看作是IO Mem,按照IO Mem的访问方式即可。

    3.    CPU是ARM或PPC架构的情况

    在这一类的嵌入式处理器中,IO Port的寻址方式是采用内存映射,也就是IO bus就是Mem bus。系统的寻址能力如果是32位,IO Port+Mem(包括IO Mem)可以达到4G。

          

    1.使用I/O 端口

    I/O 端口是驱动用来和很多设备通讯的方法。

    1.1、分配I/O 端口

    在驱动还没独占设备之前,不应对端口进行操作。内核提供了一个注册接口,以允许驱动声明其需要的端口:

    #include <linux/ioport.h>
    /* request_region告诉内核:要使用first开始的n个端口。参数name为设备名。如果分配成功返回值是非NULL;否则无法使用需要的端 口(/proc/ioports包含了系统当前所有端口的分配信息,若request_region分配失败时,可以查看该文件,看谁先用了你要的端口) */
    struct resource *request_region(unsigned long first, unsigned long n, const char *name);
    /* 用完I/O端口后(可能在模块卸载时),应当调用release_region将I/O端口返还给系统。参数start和n应与之前传递给request_region一致 */
    void release_region(unsigned long start, unsigned long n); 
    /* check_region用于检查一个给定的I/O端 口集是否可用。如果给定的端口不可用,check_region返回一个错误码。不推荐使用该函数,因为即便它返回0(端口可用),它也不能保证后面的端 口分配操作会成功,因为检查和后面的端口分配并不是一个原子操作。而request_region通过加锁来保证操作的原子性,因此是安全的 */
    int check_region(unsigned long first, unsigned long n);


          1.2、操作I/O端口

    在驱动成功请求到I/O 端口后,就可以读写这些端口了。大部分硬件会将8位、16位和32位端口区分开,无法像访问内存那样混淆使用。驱动程序必须调用不同的函数来访问不同大小的端口。

    如同前面所讲的,仅支持单地址空间的计算机体系通过将I/O端口地址重新映射到内存地址来伪装端口I/O 。为了提高移植性,内核对驱动隐藏了这些细节。Linux 内核头文件(体系依赖的头文件<asm/io.h>) 定义了下列内联函数来存取I/O端口:

    /* inb/outb:读/写字节端口(8位宽)。有些体系将port参数定义为unsigned long;而有些平台则将它定义为unsigned short。inb的返回类型也是依赖体系的 */
    unsigned inb(unsigned port);
    void outb(unsigned char byte, unsigned port);
    /* inw/outw:读/写字端口(16位宽) */
    unsigned inw(unsigned port);
    void outw(unsigned short word, unsigned port);
    /* inl/outl:读/写32位端口。longword也是依赖体系的,有的体系为unsigned long;而有的为unsigned int */
    unsigned inl(unsigned port);
    void outl(unsigned longword, unsigned port);

    从现在开始,当我们使用unsigned 没有进一步指定类型时,表示是一个依赖体系的定义。

    注意,没有64位的I/O端口操作函数。即便在64位体系中,端口地址空间使用一个32位(最大)的数据通路。

    1.3、从用户空间访问I/O端口

    1.2节介绍的函数主要是提供给驱动使用,但它们也可在用户空间使用,至少在PC机上可以。GNU C 库在 <sys/io.h> 中定义它们。如果在用户空间使用这些函数,必须满足下列条件:

    1)、程序必须使用-O选项编译来强制扩展内联函数

    2)、必须使用ioperm和iopl系统调用(#include <sys/perm.h>) 来获得进行操作I/O端口的权限。ioperm 为获取单个端口的操作许可,iopl 为获取整个I/O空间许可。这2个函数都是x86特有的

    3)、程序必须以root来调用ioperm或者iopl,或者其父进程(祖先)必须以root获得的端口操作权限

    如果平台不支持ioperm和iopl系统调用,通过使用/dev/prot设备文件,用户空间仍然可以存取I/O 端口。但是要注意的是,这个文件的定义也是依赖平台的。

    1.4、字串操作

    除了一次传递一个数据的I/O操作,某些处理器实现了一次传递一序列数据(单位可以是字节、字和双字)的特殊指令。这些所谓的字串指令,它们完成任务比一个C语言循环更快。下列宏定义实现字串操作,在某些体系上,它们通过使用单个机器指令实现;但如果目标处理器没有进行字串I/O指令,则通过执行一个紧凑的循环实现。

    字串函数的原型是:

    /* insb:从I/O端口port读取count个数据(单位字节)到以内存地址addr为开始的内存空间 */
    void insb(unsigned port, void *addr, unsigned long count);
    /* outsb:将内存地址addr开始的count个数据(单位字节)写到I/O端口port */
    void outsb(unsigned port, void *addr, unsigned long count);
    /* insw:从I/O端口port读取count个数据(单位字)到以内存地址addr为开始的内存空间 */
    void insw(unsigned port, void *addr, unsigned long count);
    /* outsw:将内存地址addr开始的count个数据(单位字)写到I/O端口port */
    void outsw(unsigned port, void *addr, unsigned long count);
    /* insl:从I/O端口port读取count个数据(单位双字)到以内存地址addr为开始的内存空间 */
    void insl(unsigned port, void *addr, unsigned long count);
    /* outsl:将内存地址addr开始的count个数据(单位双字)写到I/O端口port */
    void outsl(unsigned port, void *addr, unsigned long count);

    注意:使用字串函数时,它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。使用 inw读取端口应在必要时自行转换字节序,以匹配主机字节序。

    1.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同样的代码。

    1.6、平台依赖性

    由于自身的特性,I/O指令高度依赖于处理器,非常难以隐藏各体系间的不同。因此,大部分的关于端口 I/O的源码是平台依赖的。以下是x86和ARM所使用函数的总结:

    IA-32 (x86)

    x86_64

    这个体系支持本章介绍的所有函数;port参数的类型为unsigned short。

    ARM

    端口映射到内存,并且支持本章介绍的所有函数;port参数的类型为unsigned int;字串函数用C语言实现。

     

           2、使用 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 内存的情况进行了优化。

    2.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 函数设置一个映射。

    #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返回的地址不可以直接解引用;相反,应当使用内核提供的访问函数。

    2.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); 

    2.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 端口。


           3、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口的操作函数,有需要可以在驱动中直接使用,很方便。

     

     

     

     

     

     

    Linux系统对IO端口和IO内存的管理

    http://blog.csdn.net/ce123/article/details/7204458

     

     

    一、I/O端口

          端口(port)是接口电路中能被CPU直接访问的寄存器的地址。几乎每一种外设都是通过读写设备上的寄存器来进行的。CPU通过这些地址即端口向接口电 路中的寄存器发送命令,读取状态和传送数据。外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存 器通常被连续地编址。

    二、IO内存

           例如,在PC上可以插上一块图形卡,有2MB的存储空间,甚至可能还带有ROM,其中装有可执行代码。

    三、IO端口和IO内存的区分及联系

             这两者如何区分就涉及到硬件知识,X86体系中,具有两个地址空间:IO空间和内存空间,而RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,即内存空间。
    内存空间:内存地址寻址范围,32位操作系统内存空间为2的32次幂,即4G。
    IO空间:X86特有的一个空间,与内存空间彼此独立的地址空间,32位X86有64K的IO空间。

    IO端口:当寄存器或内存位于IO空间时,称为IO端口。一般寄存器也俗称I/O端口,或者说I/O ports,这个I/O端口可以被映射在Memory Space,也可以被映射在I/O Space。

    IO内存:当寄存器或内存位于内存空间时,称为IO内存。

     

    四、外设IO端口物理地址的编址方式

            CPU对外设IO端口物理地址的编址方式有两种:一种是I/O映射方式(I/O-mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于CPU的体系结构。

    1、统一编址

      RISC指令系统的CPU(如,PowerPC、m68k、ARM等)通常只实现一个物 理地址空间(RAM)。在这种情况下,外设I/O端口的物理地址就被映射到CPU的单一物理地址空间中,而成为内存的一部分。此时,CPU可以象访问一个 内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。

           统一编址也称为“I/O内存”方式,外设寄存器位于“内存空间”(很多外设有自己的内存、缓冲区,外设的寄存器和内存统称“I/O空间”)。

    2、独立编址

            而另外一些体系结构的CPU(典型地如X86)则为外设专门实现了一个单独地地址空间,称为“I/O地址空间”或者“I/O端口空间”。这是一个与CPU 地RAM物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专门的I/O指令(如X86的IN和OUT指令)来访 问这一空间中的地址单元(也即I/O端口)。与RAM物理地址空间相比,I/O地址空间通常都比较小,如x86 CPU的I/O空间就只有64KB(0-0xffff)。这是“I/O映射方式”的一个主要缺点。

            独立编址也称为“I/O端口”方式,外设寄存器位于“I/O(地址)空间”。

    3、优缺点

    独立编址主要优点是:
    1)、I/O端口地址不占用存储器空间;使用专门的I/O指令对端口进行操作,I/O指令短,执行速度快。
    2)、并且由于专门I/O指令与存储器访问指令有明显的区别,使程序中I/O操作和存储器操作层次清晰,程序的可读性强。
    3)、同时,由于使用专门的I/O指令访问端口,并且I/O端口地址和存储器地址是分开的,故I/O端口地址和存储器地址可以重叠,而不会相互混淆。 
    4)、译码电路比较简单(因为I/0端口的地址空间一般较小,所用地址线也就较少)。
    其缺点是:只能用专门的I/0指令,访问端口的方法不如访问存储器的方法多。

    统一编址优点:
    1)、由于对I/O设备的访问是使用访问存储器的指令,所以指令类型多,功能齐全,这不仅使访问I/O端口可实现输入/输出操作,而且还可对端口内容进行算术逻辑运算,移位等等;
    2)、另外,能给端口有较大的编址空间,这对大型控制系统和数据通信系统是很有意义的。
    这种方式的缺点是端口占用了存储器的地址空间,使存储器容量减小,另外指令长度比专门I/O指令要长,因而执行速度较慢。
            究竟采用哪一种取决于系统的总体设计。在一个系统中也可以同时使用两种方式,前提是首先要支持I/O独立编址。Intel的x86微处理器都支持I/O 独立编址,因为它们的指令系统中都有I/O指令,并设置了可以区分I/O访问和存储器访问的控制信号引脚。而一些微处理器或单片机,为了减少引脚,从而减 少芯片占用面积,不支持I/O独立编址,只能采用存储器统一编址。

    五、Linux下访问IO端口

               对于某一既定的系统,它要么是独立编址、要么是统一编址,具体采用哪一种则取决于CPU的体系结构。 如,PowerPC、m68k等采用统一编址,而X86等则采用独立编址,存在IO空间的概念。目前,大多数嵌入式微控制器如ARM、PowerPC等并 不提供I/O空间,仅有内存空间,可直接用地址、指针访问。但对于Linux内核而言,它可能用于不同的CPU,所以它必须都要考虑这两种方式,于是它采 用一种新的方法,将基于I/O映射方式的或内存映射方式的I/O端口通称为“I/O区域”(I/O region),不论你采用哪种方式,都要先申请IO区域:request_resource(),结束时释放 它:release_resource()。

    IO region是一种IO资源,因此它可以用resource结构类型来描述。

             访问IO端口有2种途径:I/O映射方式(I/O-mapped)、内存映射方式(Memory-mapped)。前一种途径不映射到内存空间,直接使用 intb()/outb()之类的函数来读写IO端口;后一种MMIO是先把IO端口映射到IO内存(“内存空间”),再使用访问IO内存的函数来访问 IO端口。

    1、I/O映射方式

           直接使用IO端口操作函数:在设备打开或驱动模块被加载时申请IO端口区域,之后使用inb(),outb()等进行端口访问,最后在设备关闭或驱动被卸载时释放IO端口范围。

     in、out、ins和outs汇编语言指令都可以访问I/O端口。内核中包含了以下辅助函数来简化这种访问:

    inb( )、inw( )、inl( )
    分别从I/O端口读取1、2或4个连续字节。后缀“b”、“w”、“l”分别代表一个字节(8位)、一个字(16位)以及一个长整型(32位)。

    inb_p( )、inw_p( )、inl_p( )
    分别从I/O端口读取1、2或4个连续字节,然后执行一条“哑元(dummy,即空指令)”指令使CPU暂停。

    outb( )、outw( )、outl( )
    分别向一个I/O端口写入1、2或4个连续字节。

    outb_p( )、outw_p( )、outl_p( )
    分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“哑元”指令使CPU暂停。

    insb( )、insw( )、insl( )
    分别从I/O端口读入以1、2或4个字节为一组的连续字节序列。字节序列的长度由该函数的参数给出。

    outsb( )、outsw( )、outsl( )
    分别向I/O端口写入以1、2或4个字节为一组的连续字节序列。

    流程如下:

          虽然访问I/O端口非常简单,但是检测哪些I/O端口已经分配给I/O设备可能就不这么简单了,对基于ISA总线的系统来说更是如此。通常,I/O设备驱 动程序为了探测硬件设备,需要盲目地向某一I/O端口写入数据;但是,如果其他硬件设备已经使用这个端口,那么系统就会崩溃。为了防止这种情况的发生,内 核必须使用“资源”来记录分配给每个硬件设备的I/O端口。资源表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。在这里,资源表示I/O端口地 址的一个范围。每个资源对应的信息存放在resource数据结构中:

    [plain] view plaincopy

     

    1. struct resource {  
    2.          resource_size_t start;// 资源范围的开始  
    3.          resource_size_t end;// 资源范围的结束  
    4.          const char *name; //资源拥有者的名字  
    5.          unsigned long flags;// 各种标志  
    6.          struct resource *parent, *sibling, *child;// 指向资源树中父亲,兄弟和孩子的指针  
    7. };  


              所有的同种资源都插入到一个树型数据结构(父亲、兄弟和孩子)中;例如,表示I/O端口地址范围的所有资源都包括在一个根节点为 ioport_resource的树中。节点的孩子被收集在一个链表中,其第一个元素由child指向。sibling字段指向链表中的下一个节点。

            为什么使用树?例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000 到 0xf00f。那么,start字段为0xf000 且end 字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。但是,IDE设备驱动程序需要记住另外的信息,也就是IDE链主盘 使用0xf000 到0xf007的子范围,从盘使用0xf008 到0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000 到0xf00f的整个范围对应的资源下。一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。I/O端口资源树 (ioport_resource)的根节点跨越了整个I/O地址空间(从端口0到65535)。

    任何设备驱动程序都可以使用下面三个函数,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:

    request_resource( )        //把一个给定范围分配给一个I/O设备。

    allocate_resource( )        //在资源树中寻找一个给定大小和排列方式的可用范围;若存在,将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,可以使用任意的端口号和主板上的内存地址对其进行配置)。

    release_resource( )      //释放以前分配给I/O设备的给定范围。

    内核也为以上函数定义了一些应用于I/O端口的快捷函数:request_region( )分配I/O端口的给定范围,release_region( )释放以前分配给I/O端口的范围。当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。

    2、内存映射方式

             将IO端口映射为内存进行访问,在设备打开或驱动模块被加载时,申请IO端口区域并使用ioport_map()映射到内存,之后使用IO内存的函数进行端口访问,最后,在设备关闭或驱动模块被卸载时释放IO端口并释放映射。

    映射函数的原型为:
    void *ioport_map(unsigned long port, unsigned int count);
    通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。但请注意,在进行映射前,还必须通过request_region( )分配I/O端口。

    当不再需要这种映射时,需要调用下面的函数来撤消:
    void ioport_unmap(void *addr);

    在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是宜使用Linux内核的如下一组函数来完成访问I/O内存:·读I/O内存
    unsigned int ioread8(void *addr);
    unsigned int ioread16(void *addr);
    unsigned int ioread32(void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    unsigned readb(address);
    unsigned readw(address);
    unsigned readl(address);
    ·写I/O内存
    void iowrite8(u8 value, void *addr);
    void iowrite16(u16 value, void *addr);
    void iowrite32(u32 value, void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    void writeb(unsigned value, address);
    void writew(unsigned value, address);
    void writel(unsigned value, address);

    流程如下:

    六、Linux下访问IO内存

             IO内存的访问方法是:首先调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间的虚拟地址, 之后就可以Linux设备访问编程接口访问这些寄存器了,访问完成后,使用ioremap()对申请的虚拟地址进行释放,并释放 release_mem_region()申请的IO内存资源。

    struct resource *requset_mem_region(unsigned long start, unsigned long len,char *name);
       这个函数从内核申请len个内存地址(在3G~4G之间的虚地址),而这里的start为I/O物理地址,name为设备的名称。注意,。如果分配成功,则返回非NULL,否则,返回NULL。
    另外,可以通过/proc/iomem查看系统给各种设备的内存范围。

    要释放所申请的I/O内存,应当使用release_mem_region()函数:
    void release_mem_region(unsigned long start, unsigned long len)

    申请一组I/O内存后, 调用ioremap()函数:
    void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
    其中三个参数的含义为:
    phys_addr:与requset_mem_region函数中参数start相同的I/O物理地址;
    size:要映射的空间的大小;
    flags:要映射的IO空间的和权限有关的标志;

    功能:将一个I/O地址空间映射到内核的虚拟地址空间上(通过release_mem_region()申请到的)

    流程如下:

    六、ioremap和ioport_map

    下面具体看一下ioport_map和ioport_umap的源码:

    [plain] view plaincopy

     

    1. void __iomem *ioport_map(unsigned long port, unsigned int nr)  
    2. {  
    3.     if (port > PIO_MASK)  
    4.         return NULL;  
    5.     return (void __iomem *) (unsigned long) (port + PIO_OFFSET);  
    6. }  
    7.   
    8. void ioport_unmap(void __iomem *addr)  
    9. {  
    10.     /* Nothing to do */  
    11. }  

              ioport_map仅仅是将port加上PIO_OFFSET(64k),而ioport_unmap则什么都不做。这样portio的64k空间就被映射到虚拟地址的64k~128k之间,而ioremap返回的虚拟地址则肯定在3G之上。ioport_map函数的目的是试图提供与ioremap一致的虚拟地址空间。分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口ioread8/iowrite8(......)访问I/O端口。
              最后来看一下ioread8的源码,其实现也就是对虚拟地址进行了判断,以区分IO端口和IO内存,然后分别使用inb/outb和readb/writeb来读写。

    [plain] view plaincopy

     

    1. unsigned int fastcall ioread8(void __iomem *addr)  
    2. {  
    3.     IO_COND(addr, return inb(port), return readb(addr));  
    4. }  
    5.   
    6. #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)  
    7. #define IO_COND(addr, is_pio, is_mmio) do { \  
    8.     unsigned long port = (unsigned long __force)addr; \  
    9.         if (port < PIO_RESERVED) { \  
    10.             VERIFY_PIO(port); \  
    11.             port &= PIO_MASK; \  
    12.             is_pio; \  
    13.         } else { \  
    14.             is_mmio; \  
    15.         } \  
    16. } while (0)  
    17.   
    18. 展开:  
    19. unsigned int fastcall ioread8(void __iomem *addr)  
    20. {  
    21.     unsigned long port = (unsigned long __force)addr;  
    22.     if( port < 0x40000UL ) {  
    23.         BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );  
    24.         port &= PIO_MASK;  
    25.         return inb(port);  
    26.     }else{  
    27.         return readb(addr);  
    28.     }  
    29. }  

    七、总结

             外设IO寄存器地址独立编址的CPU,这时应该称外设IO寄存器为IO端 口,访问IO寄存器可通过ioport_map将其映射到虚拟地址空间,但实际上这是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为 了可以使用和IO内存一样的接口访问IO寄存器;也可以直接使用in/out指令访问IO寄存器。

              例如:Intel x86平台普通使用了名为内存映射(MMIO)的技术,该技术是PCI规范的一部分,IO设备端口被映射到内存空间,映射后,CPU访问IO端口就如同访 问内存一样。

              外设IO寄存器地址统一编址的CPU,这时应该称外设IO寄存器为IO内存,访问IO寄存器可通过ioremap将其映射到虚拟地址空间,然后再使用read/write接口访问。

    展开全文
  • IO端口与IO内存

    2014-06-16 14:33:16
    IO端口和IO内存的区别及分别使用的函数接口 分类: 苦与乐---linux 2012-09-23 23:29 5232人阅读 评论(6) 收藏 举报 iolinux内核存储x86数据结构linux IO端口和IO内存的区别及分别使用的函数接口 ...

    IO端口和IO内存的区别及分别使用的函数接口

    分类: 苦与乐---linux 5232人阅读 评论(6) 收藏 举报

    IO端口和IO内存的区别及分别使用的函数接口 

             每个外设都是通过读写其寄存器来控制的。外设寄存器也称为I/O端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。根据访问外设寄存器的不同方式,可以把CPU分成两大类。一类CPU(如M68K,Power PC等)把这些寄存器看作内存的一部分,寄存器参与内存统一编址访问寄存器就通过访问一般的内存指令进行,所以,这种CPU没有专门用于设备I/O的指令。这就是所谓的“I/O内存”方式。另一类CPU(典型的如X86),将外设的寄存器看成一个独立的地址空间,所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令。这就是所谓的“ I/O端口”方式。但是,用于I/O指令的“地址空间”相对来说是很小的,如x86 CPU的I/O空间就只有64KB(0-0xffff)。

            结合下图,我们彻底讲述IO端口和IO内存以及内存之间的关系。主存16M字节的SDRAM,外设是个视频采集卡,上面有16M字节的SDRAM作为缓冲区。


    1.   CPU是i386架构的情况

     在i386系列的处理中,内存和外部IO是独立编址,也是独立寻址的。MEM的内存空间是32位可以寻址到4G,IO空间是16位可以寻址到64K。

     在Linux内核中,访问外设上的IO Port必须通过IO Port的寻址方式。而访问IO Mem就比较罗嗦,外部MEM不能和主存一样访问,虽然大小上不相上下,可是外部MEM是没有在系统中注册的。访问外部IO MEM必须通过remap映射到内核的MEM空间后才能访问。为了达到接口的同一性,内核提供了IO Port到IO Mem的映射函数。映射后IO Port就可以看作是IO Mem,按照IO Mem的访问方式即可。

    3.    CPU是ARM或PPC架构的情况

    在这一类的嵌入式处理器中,IO Port的寻址方式是采用内存映射,也就是IO bus就是Mem bus。系统的寻址能力如果是32位,IO Port+Mem(包括IO Mem)可以达到4G。

          

    1.使用I/O 端口

    I/O 端口是驱动用来和很多设备通讯的方法。

    1.1、分配I/O 端口

    在驱动还没独占设备之前,不应对端口进行操作。内核提供了一个注册接口,以允许驱动声明其需要的端口:

    #include <linux/ioport.h>
    /* request_region告诉内核:要使用first开始的n个端口。参数name为设备名。如果分配成功返回值是非NULL;否则无法使用需要的端口(/proc/ioports包含了系统当前所有端口的分配信息,若request_region分配失败时,可以查看该文件,看谁先用了你要的端口) */
    struct resource *request_region(unsigned long first, unsigned long n, const char *name);
    /* 用完I/O端口后(可能在模块卸载时),应当调用release_region将I/O端口返还给系统。参数start和n应与之前传递给request_region一致 */
    void release_region(unsigned long start, unsigned long n); 
    /* check_region用于检查一个给定的I/O端口集是否可用。如果给定的端口不可用,check_region返回一个错误码。不推荐使用该函数,因为即便它返回0(端口可用),它也不能保证后面的端口分配操作会成功,因为检查和后面的端口分配并不是一个原子操作。而request_region通过加锁来保证操作的原子性,因此是安全的 */
    int check_region(unsigned long first, unsigned long n);


          1.2、操作I/O端口

    在驱动成功请求到I/O 端口后,就可以读写这些端口了。大部分硬件会将8位、16位和32位端口区分开,无法像访问内存那样混淆使用。驱动程序必须调用不同的函数来访问不同大小的端口。

    如同前面所讲的,仅支持单地址空间的计算机体系通过将I/O端口地址重新映射到内存地址来伪装端口I/O 。为了提高移植性,内核对驱动隐藏了这些细节。Linux 内核头文件(体系依赖的头文件<asm/io.h>) 定义了下列内联函数来存取I/O端口:

    /* inb/outb:读/写字节端口(8位宽)。有些体系将port参数定义为unsigned long;而有些平台则将它定义为unsigned short。inb的返回类型也是依赖体系的 */
    unsigned inb(unsigned port);
    void outb(unsigned char byte, unsigned port);
    /* inw/outw:读/写字端口(16位宽) */
    unsigned inw(unsigned port);
    void outw(unsigned short word, unsigned port);
    /* inl/outl:读/32位端口。longword也是依赖体系的,有的体系为unsigned long;而有的为unsigned int */
    unsigned inl(unsigned port);
    void outl(unsigned longword, unsigned port);

    从现在开始,当我们使用unsigned 没有进一步指定类型时,表示是一个依赖体系的定义。

    注意,没有64位的I/O端口操作函数。即便在64位体系中,端口地址空间使用一个32位(最大)的数据通路。

    1.3、从用户空间访问I/O端口

    1.2节介绍的函数主要是提供给驱动使用,但它们也可在用户空间使用,至少在PC机上可以。GNU C 库在 <sys/io.h> 中定义它们。如果在用户空间使用这些函数,必须满足下列条件:

    1)、程序必须使用-O选项编译来强制扩展内联函数

    2)、必须使用ioperm和iopl系统调用(#include <sys/perm.h>) 来获得进行操作I/O端口的权限。ioperm 为获取单个端口的操作许可,iopl 为获取整个I/O空间许可。这2个函数都是x86特有的

    3)、程序必须以root来调用ioperm或者iopl,或者其父进程(祖先)必须以root获得的端口操作权限

    如果平台不支持ioperm和iopl系统调用,通过使用/dev/prot设备文件,用户空间仍然可以存取I/O 端口。但是要注意的是,这个文件的定义也是依赖平台的。

    1.4、字串操作

    除了一次传递一个数据的I/O操作,某些处理器实现了一次传递一序列数据(单位可以是字节、字和双字)的特殊指令。这些所谓的字串指令,它们完成任务比一个C语言循环更快。下列宏定义实现字串操作,在某些体系上,它们通过使用单个机器指令实现;但如果目标处理器没有进行字串I/O指令,则通过执行一个紧凑的循环实现。

    字串函数的原型是:

    /* insb:从I/O端口port读取count个数据(单位字节)到以内存地址addr为开始的内存空间 */
    void insb(unsigned port, void *addr, unsigned long count);
    /* outsb:将内存地址addr开始的count个数据(单位字节)写到I/O端口port */
    void outsb(unsigned port, void *addr, unsigned long count);
    /* insw:从I/O端口port读取count个数据(单位字)到以内存地址addr为开始的内存空间 */
    void insw(unsigned port, void *addr, unsigned long count);
    /* outsw:将内存地址addr开始的count个数据(单位字)写到I/O端口port */
    void outsw(unsigned port, void *addr, unsigned long count);
    /* insl:从I/O端口port读取count个数据(单位双字)到以内存地址addr为开始的内存空间 */
    void insl(unsigned port, void *addr, unsigned long count);
    /* outsl:将内存地址addr开始的count个数据(单位双字)写到I/O端口port */
    void outsl(unsigned port, void *addr, unsigned long count);

    注意:使用字串函数时,它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。使用 inw读取端口应在必要时自行转换字节序,以匹配主机字节序。

    1.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同样的代码。

    1.6、平台依赖性

    由于自身的特性,I/O指令高度依赖于处理器,非常难以隐藏各体系间的不同。因此,大部分的关于端口 I/O的源码是平台依赖的。以下是x86和ARM所使用函数的总结:

    IA-32 (x86)

    x86_64

    这个体系支持本章介绍的所有函数;port参数的类型为unsigned short。

    ARM

    端口映射到内存,并且支持本章介绍的所有函数;port参数的类型为unsigned int;字串函数用C语言实现。

     

           2、使用 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 内存的情况进行了优化。

    2.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 函数设置一个映射

    #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返回的地址不可以直接解引用;相反,应当使用内核提供的访问函数。

    2.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); 

    2.3、像I/O 内存一样使用端口

    一些硬件有一个有趣的特性: 有些版本使用 I/O 端口;而有些版本则使用 I/O 内存。不管是I/O 端口还是I/O 内存,处理器见到的设备寄存器都是相同的,只是访问方法不同。为了统一编程接口,使驱动程序易于编写,2.6内核提供了一个ioport_map函数:

    /* ioport_map重新映射countI/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 端口。


           3、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口的操作函数,有需要可以在驱动中直接使用,很方便。






    Linux系统对IO端口和IO内存的管理

    http://blog.csdn.net/ce123/article/details/7204458


    一、I/O端口

          端口(port)是接口电路中能被CPU直接访问的寄存器的地址。几乎每一种外设都是通过读写设备上的寄存器来进行的。CPU通过这些地址即端口向接口电路中的寄存器发送命令,读取状态和传送数据。外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连续地编址。

    二、IO内存

           例如,在PC上可以插上一块图形卡,有2MB的存储空间,甚至可能还带有ROM,其中装有可执行代码。

    三、IO端口和IO内存的区分及联系

             这两者如何区分就涉及到硬件知识,X86体系中,具有两个地址空间:IO空间和内存空间,而RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,即内存空间。
    内存空间:内存地址寻址范围,32位操作系统内存空间为2的32次幂,即4G。
    IO空间:X86特有的一个空间,与内存空间彼此独立的地址空间,32位X86有64K的IO空间。

    IO端口:当寄存器或内存位于IO空间时,称为IO端口。一般寄存器也俗称I/O端口,或者说I/O ports,这个I/O端口可以被映射在Memory Space,也可以被映射在I/O Space。

    IO内存:当寄存器或内存位于内存空间时,称为IO内存。

     

    四、外设IO端口物理地址的编址方式

            CPU对外设IO端口物理地址的编址方式有两种:一种是I/O映射方式(I/O-mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于CPU的体系结构。

    1、统一编址

      RISC指令系统的CPU(如,PowerPC、m68k、ARM等)通常只实现一个物理地址空间(RAM)。在这种情况下,外设I/O端口的物理地址就被映射到CPU的单一物理地址空间中,而成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。

           统一编址也称为“I/O内存”方式,外设寄存器位于“内存空间”(很多外设有自己的内存、缓冲区,外设的寄存器和内存统称“I/O空间”)。

    2、独立编址

            而另外一些体系结构的CPU(典型地如X86)则为外设专门实现了一个单独地地址空间,称为“I/O地址空间”或者“I/O端口空间”。这是一个与CPU地RAM物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元(也即I/O端口)。与RAM物理地址空间相比,I/O地址空间通常都比较小,如x86 CPU的I/O空间就只有64KB(0-0xffff)。这是“I/O映射方式”的一个主要缺点。

            独立编址也称为“I/O端口”方式,外设寄存器位于“I/O(地址)空间”。

    3、优缺点

    独立编址主要优点是:
    1)、I/O端口地址不占用存储器空间;使用专门的I/O指令对端口进行操作,I/O指令短,执行速度快。
    2)、并且由于专门I/O指令与存储器访问指令有明显的区别,使程序中I/O操作和存储器操作层次清晰,程序的可读性强。
    3)、同时,由于使用专门的I/O指令访问端口,并且I/O端口地址和存储器地址是分开的,故I/O端口地址和存储器地址可以重叠,而不会相互混淆。 
    4)、译码电路比较简单(因为I/0端口的地址空间一般较小,所用地址线也就较少)。
    其缺点是:只能用专门的I/0指令,访问端口的方法不如访问存储器的方法多。

    统一编址优点:
    1)、由于对I/O设备的访问是使用访问存储器的指令,所以指令类型多,功能齐全,这不仅使访问I/O端口可实现输入/输出操作,而且还可对端口内容进行算术逻辑运算,移位等等;
    2)、另外,能给端口有较大的编址空间,这对大型控制系统和数据通信系统是很有意义的。
    这种方式的缺点是端口占用了存储器的地址空间,使存储器容量减小,另外指令长度比专门I/O指令要长,因而执行速度较慢。

            究竟采用哪一种取决于系统的总体设计。在一个系统中也可以同时使用两种方式,前提是首先要支持I/O独立编址。Intel的x86微处理器都支持I/O 独立编址,因为它们的指令系统中都有I/O指令,并设置了可以区分I/O访问和存储器访问的控制信号引脚。而一些微处理器或单片机,为了减少引脚,从而减 少芯片占用面积,不支持I/O独立编址,只能采用存储器统一编址。

    五、Linux下访问IO端口

               对于某一既定的系统,它要么是独立编址、要么是统一编址,具体采用哪一种则取决于CPU的体系结构。 如,PowerPC、m68k等采用统一编址,而X86等则采用独立编址,存在IO空间的概念。目前,大多数嵌入式微控制器如ARM、PowerPC等并不提供I/O空间,仅有内存空间,可直接用地址、指针访问。但对于Linux内核而言,它可能用于不同的CPU,所以它必须都要考虑这两种方式,于是它采用一种新的方法,将基于I/O映射方式的或内存映射方式的I/O端口通称为“I/O区域”(I/O region),不论你采用哪种方式,都要先申请IO区域:request_resource(),结束时释放它:release_resource()。

    IO region是一种IO资源,因此它可以用resource结构类型来描述。

             访问IO端口有2种途径:I/O映射方式(I/O-mapped)、内存映射方式(Memory-mapped)。前一种途径不映射到内存空间,直接使用 intb()/outb()之类的函数来读写IO端口;后一种MMIO是先把IO端口映射到IO内存(“内存空间”),再使用访问IO内存的函数来访问 IO端口。

    1、I/O映射方式

           直接使用IO端口操作函数:在设备打开或驱动模块被加载时申请IO端口区域,之后使用inb(),outb()等进行端口访问,最后在设备关闭或驱动被卸载时释放IO端口范围。

     in、out、ins和outs汇编语言指令都可以访问I/O端口。内核中包含了以下辅助函数来简化这种访问:

    inb( )、inw( )、inl( )
    分别从I/O端口读取1、2或4个连续字节。后缀“b”、“w”、“l”分别代表一个字节(8位)、一个字(16位)以及一个长整型(32位)。

    inb_p( )、inw_p( )、inl_p( )
    分别从I/O端口读取1、2或4个连续字节,然后执行一条“哑元(dummy,即空指令)”指令使CPU暂停。

    outb( )、outw( )、outl( )
    分别向一个I/O端口写入1、2或4个连续字节。

    outb_p( )、outw_p( )、outl_p( )
    分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“哑元”指令使CPU暂停。

    insb( )、insw( )、insl( )
    分别从I/O端口读入以1、2或4个字节为一组的连续字节序列。字节序列的长度由该函数的参数给出。

    outsb( )、outsw( )、outsl( )
    分别向I/O端口写入以1、2或4个字节为一组的连续字节序列。

    流程如下:

          虽然访问I/O端口非常简单,但是检测哪些I/O端口已经分配给I/O设备可能就不这么简单了,对基于ISA总线的系统来说更是如此。通常,I/O设备驱动程序为了探测硬件设备,需要盲目地向某一I/O端口写入数据;但是,如果其他硬件设备已经使用这个端口,那么系统就会崩溃。为了防止这种情况的发生,内核必须使用“资源”来记录分配给每个硬件设备的I/O端口。资源表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。在这里,资源表示I/O端口地址的一个范围。每个资源对应的信息存放在resource数据结构中:

    [plain]  view plain copy
    1. struct resource {  
    2.          resource_size_t start;// 资源范围的开始  
    3.          resource_size_t end;// 资源范围的结束  
    4.          const char *name; //资源拥有者的名字  
    5.          unsigned long flags;// 各种标志  
    6.          struct resource *parent, *sibling, *child;// 指向资源树中父亲,兄弟和孩子的指针  
    7. };  


              所有的同种资源都插入到一个树型数据结构(父亲、兄弟和孩子)中;例如,表示I/O端口地址范围的所有资源都包括在一个根节点为ioport_resource的树中。节点的孩子被收集在一个链表中,其第一个元素由child指向。sibling字段指向链表中的下一个节点。

            为什么使用树?例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000 到 0xf00f。那么,start字段为0xf000 且end 字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。但是,IDE设备驱动程序需要记住另外的信息,也就是IDE链主盘使用0xf000 到0xf007的子范围,从盘使用0xf008 到0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000 到0xf00f的整个范围对应的资源下。一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。I/O端口资源树(ioport_resource)的根节点跨越了整个I/O地址空间(从端口0到65535)。

    任何设备驱动程序都可以使用下面三个函数,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:

    request_resource( )        //把一个给定范围分配给一个I/O设备。

    allocate_resource( )        //在资源树中寻找一个给定大小和排列方式的可用范围;若存在,将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,可以使用任意的端口号和主板上的内存地址对其进行配置)。

    release_resource( )      //释放以前分配给I/O设备的给定范围。

    内核也为以上函数定义了一些应用于I/O端口的快捷函数:request_region( )分配I/O端口的给定范围,release_region( )释放以前分配给I/O端口的范围。当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。

    2、内存映射方式

             将IO端口映射为内存进行访问,在设备打开或驱动模块被加载时,申请IO端口区域并使用ioport_map()映射到内存,之后使用IO内存的函数进行端口访问,最后,在设备关闭或驱动模块被卸载时释放IO端口并释放映射。

    映射函数的原型为:
    void *ioport_map(unsigned long port, unsigned int count);
    通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。但请注意,在进行映射前,还必须通过request_region( )分配I/O端口。

    当不再需要这种映射时,需要调用下面的函数来撤消:
    void ioport_unmap(void *addr);

    在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是宜使用Linux内核的如下一组函数来完成访问I/O内存:·读I/O内存
    unsigned int ioread8(void *addr);
    unsigned int ioread16(void *addr);
    unsigned int ioread32(void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    unsigned readb(address);
    unsigned readw(address);
    unsigned readl(address);
    ·写I/O内存
    void iowrite8(u8 value, void *addr);
    void iowrite16(u16 value, void *addr);
    void iowrite32(u32 value, void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    void writeb(unsigned value, address);
    void writew(unsigned value, address);
    void writel(unsigned value, address);

    流程如下:

    六、Linux下访问IO内存

             IO内存的访问方法是:首先调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间的虚拟地址,之后就可以Linux设备访问编程接口访问这些寄存器了,访问完成后,使用ioremap()对申请的虚拟地址进行释放,并释放release_mem_region()申请的IO内存资源。

    struct resource *requset_mem_region(unsigned long start, unsigned long len,char *name);
       这个函数从内核申请len个内存地址(在3G~4G之间的虚地址),而这里的start为I/O物理地址,name为设备的名称。注意,。如果分配成功,则返回非NULL,否则,返回NULL。
    另外,可以通过/proc/iomem查看系统给各种设备的内存范围。

    要释放所申请的I/O内存,应当使用release_mem_region()函数:
    void release_mem_region(unsigned long start, unsigned long len)

    申请一组I/O内存后, 调用ioremap()函数:
    void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
    其中三个参数的含义为:
    phys_addr:与requset_mem_region函数中参数start相同的I/O物理地址;
    size:要映射的空间的大小;
    flags:要映射的IO空间的和权限有关的标志;

    功能:将一个I/O地址空间映射到内核的虚拟地址空间上(通过release_mem_region()申请到的)

    流程如下:

    六、ioremap和ioport_map

    下面具体看一下ioport_map和ioport_umap的源码:

    [plain]  view plain copy
    1. void __iomem *ioport_map(unsigned long port, unsigned int nr)  
    2. {  
    3.     if (port > PIO_MASK)  
    4.         return NULL;  
    5.     return (void __iomem *) (unsigned long) (port + PIO_OFFSET);  
    6. }  
    7.   
    8. void ioport_unmap(void __iomem *addr)  
    9. {  
    10.     /* Nothing to do */  
    11. }  

              ioport_map仅仅是将port加上PIO_OFFSET(64k),而ioport_unmap则什么都不做。这样portio的64k空间就被映射到虚拟地址的64k~128k之间,而ioremap返回的虚拟地址则肯定在3G之上。ioport_map函数的目的是试图提供与ioremap一致的虚拟地址空间。分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口ioread8/iowrite8(......)访问I/O端口。
              最后来看一下ioread8的源码,其实现也就是对虚拟地址进行了判断,以区分IO端口和IO内存,然后分别使用inb/outb和readb/writeb来读写。

    [plain]  view plain copy
    1. unsigned int fastcall ioread8(void __iomem *addr)  
    2. {  
    3.     IO_COND(addr, return inb(port), return readb(addr));  
    4. }  
    5.   
    6. #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)  
    7. #define IO_COND(addr, is_pio, is_mmio) do { \  
    8.     unsigned long port = (unsigned long __force)addr; \  
    9.         if (port < PIO_RESERVED) { \  
    10.             VERIFY_PIO(port); \  
    11.             port &= PIO_MASK; \  
    12.             is_pio; \  
    13.         } else { \  
    14.             is_mmio; \  
    15.         } \  
    16. } while (0)  
    17.   
    18. 展开:  
    19. unsigned int fastcall ioread8(void __iomem *addr)  
    20. {  
    21.     unsigned long port = (unsigned long __force)addr;  
    22.     if( port < 0x40000UL ) {  
    23.         BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );  
    24.         port &= PIO_MASK;  
    25.         return inb(port);  
    26.     }else{  
    27.         return readb(addr);  
    28.     }  
    29. }  

    七、总结

             外设IO寄存器地址独立编址的CPU,这时应该称外设IO寄存器为IO端口,访问IO寄存器可通过ioport_map将其映射到虚拟地址空间,但实际上这是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了可以使用和IO内存一样的接口访问IO寄存器;也可以直接使用in/out指令访问IO寄存器。

              例如:Intel x86平台普通使用了名为内存映射(MMIO)的技术,该技术是PCI规范的一部分,IO设备端口被映射到内存空间,映射后,CPU访问IO端口就如同访 问内存一样。

              外设IO寄存器地址统一编址的CPU,这时应该称外设IO寄存器为IO内存,访问IO寄存器可通过ioremap将其映射到虚拟地址空间,然后再使用read/write接口访问。


    展开全文
  • io端口和io内存

    千次阅读 2012-12-11 16:19:44
    linux中的 IO端口映射和IO内存映射 (一)地址的概念 1)物理地址:CPU地址总线传来的地址,由硬件电路控制其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上 (如显存、...

    linux中的 IO端口映射和IO内存映射

    (一)地址的概念

    1)物理地址:CPU地址总线传来的地址,由硬件电路控制其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上 (如显存、BIOS等)。在程序指令中的虚拟地址经过段映射和页面映射后,就生成了物理地址,这个物理地址被放到CPU的地址线上。
            物理地址空间,一部分给物理RAM(内存)用,一部分给总线用,这是由硬件设计来决定的,因此在32 bits地址线的x86处理器中,物理地址空间是2的32次方,即4GB,但物理RAM一般不能上到4GB,因为还有一部分要给总线用(总线上还挂着别的 许多设备)。在PC机中,一般是把低端物理地址给RAM用,高端物理地址给总线用。
     
    2)总线地址:总线的地址线或在地址周期上产生的信号。外设使用的是总线地址,CPU使用的是物理地址。
            物理地址与总线地址之间的关系由系统的设计决定的。在x86平台上,物理地址就是总线地址,这是因为它们共享相同的地址空间——这句话有点难理解,详见下 面的“独立编址”。在其他平台上,可能需要转换/映射。比如:CPU需要访问物理地址是0xfa000的单元,那么在x86平台上,会产生一个PCI总线 上对0xfa000地址的访问。因为物理地址和总线地址相同,所以凭眼睛看是不能确定这个地址是用在哪儿的,它或者在内存中,或者是某个卡上的存储单元, 甚至可能这个地址上没有对应的存储器。

    3)虚拟地址:现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要MMU(Memory Management Unit)的支持。MMU通常是CPU的一部分,如果处理器没有MMU,或者有MMU但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被 内存芯片(物理内存)接收,这称为物理地址(Physical Address),如果处理器启用了MMU,CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址。
            Linux中,进程的4GB(虚拟)内存分为用户空间、内核空间。用户空间分布为0~3GB(即PAGE_OFFSET,在0X86中它等于0xC0000000),剩下的1G为内核空间。程序员只能使用虚拟地址。系统中每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。
            CPU发出取指令请求时的地址是当前上下文的虚拟地址,MMU再从页表中找到这个虚拟地址的物理地址,完成取指。同样读取数据的也是虚拟地址,比如mov ax, var. 编译时var就是一个虚拟地址,也是通过MMU从也表中来找到物理地址,再产生总线时序,完成取数据的。


    (二)编址方式
    1)外设都是通过读写设备上的寄存器来进行的,外设寄存器也称为“I/O端口”,而IO端口有两种编址方式:独立编址和统一编制。

            统一编址:外设接口中的IO寄存器(即IO端口)与主存单元一样看待,每个端口占用一个存储单元的地址,将主存的一部分划出来用作IO地址空间,如,在 PDP-11中,把最高的4K主存作为IO设备寄存器地址。端口占用了存储器的地址空间,使存储量容量减小。
            统一编址也称为“I/O内存”方式,外设寄存器位于“内存空间”(很多外设有自己的内存、缓冲区,外设的寄存器和内存统称“I/O空间”)。
            如,Samsung的S3C2440,是32位ARM处理器,它的4GB地址空间被外设、RAM等瓜分:
    0x8000 1000    LED 8*8点阵的地址
    0x4800 0000 ~ 0x6000 0000  SFR(特殊暂存器)地址空间
    0x3800 1002   键盘地址
    0x3000 0000 ~ 0x3400 0000  SDRAM空间 
    0x2000 0020 ~ 0x2000 002e  IDE
    0x1900 0300   CS8900

            独立编址(单独编址):IO地址与存储地址分开独立编址,I/0端口地址不占用存储空间的地址范围,这样,在系统中就存在了另一种与存储地址无关的IO地 址,CPU也必须具有专用与输入输出操作的IO指令(IN、OUT等)和控制逻辑。独立编址下,地址总线上过来一个地址,设备不知道是给IO端口的、还是 给存储器的,于是处理器通过MEMR/MEMW和IOR/IOW两组控制信号来实现对I/O端口和存储器的不同寻址。如,intel 80x86就采用单独编址,CPU内存和I/O是一起编址的,就是说内存一部分的地址和I/O地址是重叠的。
            独立编址也称为“I/O端口”方式,外设寄存器位于“I/O(地址)空间”。
            对于x86架构来说,通过IN/OUT指令访问。PC架构一共有65536个8bit的I/O端口,组成64K个I/O地址空间,编号从 0~0xFFFF,有16位,80x86用低16位地址线A0-A15来寻址。连续两个8bit的端口可以组成一个16bit的端口,连续4个组成一个 32bit的端口。I/O地址空间和CPU的物理地址空间是两个不同的概念,例如I/O地址空间为64K,一个32bit的CPU物理地址空间是4G。 如,在Intel 8086+Redhat9.0 下用“more /proc/ioports”可看到:
    0000-001f : dma1
    0020-003f : pic1
    0040-005f : timer
    0060-006f : keyboard
    0070-007f : rtc
    0080-008f : dma page reg
    00a0-00bf : pic2
    00c0-00df : dma2
    00f0-00ff : fpu
    0170-0177 : ide1
    ……

            不过Intel x86平台普通使用了名为内存映射(MMIO)的技术,该技术是PCI规范的一部分,IO设备端口被映射到内存空间,映射后,CPU访问IO端口就如同访 问内存一样。看Intel TA 719文档给出的x86/x64系统典型内存地址分配表:
    系统资源  占用
    ------------------------------------------------------------------------
    BIOS  1M
    本地APIC  4K
    芯片组保留 2M
    IO APIC  4K
    PCI设备  256M
    PCI Express设备 256M
    PCI设备(可选) 256M
    显示帧缓存 16M
    TSEG  1M


            对于某一既定的系统,它要么是独立编址、要么是统一编址,具体采用哪一种则取决于CPU的体系结构。 如,PowerPC、m68k等采用统一编址,而X86等则采用独立编址,存在IO空间的概念。目前,大多数嵌入式微控制器如ARM、PowerPC等并 不提供I/O空间,仅有内存空间,可直接用地址、指针访问。但对于Linux内核而言,它可能用于不同的CPU,所以它必须都要考虑这两种方式,于是它采 用一种新的方法,将基于I/O映射方式的或内存映射方式的I/O端口通称为“I/O区域”(I/O region),不论你采用哪种方式,都要先申请IO区域:request_resource(),结束时释放 它:release_resource()。
     

    2)对外设的访问

    1、访问I/O内存的流程是:request_mem_region() -> ioremap() -> ioread8()/iowrite8() -> iounmap() ->release_mem_region() 
            前面说过,IO内存是统一编址下的概念,对于统一编址,IO地址空间是物理主存的一部分,对于编程而言,我们只能操作虚拟内存,所以,访问的第一步就是要把设备所处的物理地址映射到虚拟地址,Linux2.6下用ioremap():
            void *ioremap(unsigned long offset, unsigned long size);
    然后,我们可以直接通过指针来访问这些地址,但是也可以用Linux内核的一组函数来读写:
    ioread8(), iowrite16(), ioread8_rep(), iowrite8_rep()......

    2、访问I/O端口
            访问IO端口有2种途径:I/O映射方式(I/O-mapped)、内存映射方式(Memory-mapped)。前一种途径不映射到内存空间,直接使用 intb()/outb()之类的函数来读写IO端口;后一种MMIO是先把IO端口映射到IO内存(“内存空间”),再使用访问IO内存的函数来访问 IO端口。
            void ioport_map(unsigned long port, unsigned int count);
    通过这个函数,可以把port开始的count个连续的IO端口映射为一段“内存空间”,然后就可以在其返回的地址是像访问IO内存一样访问这些IO端口。
     
     
     
    Linux下的IO端口和IO内存
     
    CPU对外设端口物理地址的编址方式有两种:一种是IO映射方式,另一种是内存映射方式。 
     Linux将基于IO映射方式的和内存映射方式的IO端口统称为IO区域(IO region)。


      IO region仍然是一种IO资源,因此它仍然可以用resource结构类型来描述。

      Linux管理IO region:

      1) request_region()

      把一个给定区间的IO端口分配给一个IO设备。

      2) check_region()

      检查一个给定区间的IO端口是否空闲,或者其中一些是否已经分配给某个IO设备。

      3) release_region()

      释放以前分配给一个IO设备的给定区间的IO端口。

      Linux中可以通过以下辅助函数来访问IO端口:

      inb(),inw(),inl(),outb(),outw(),outl()

      “b”“w”“l”分别代表8位,16位,32位。

     对IO内存资源的访问

      1) request_mem_region()

      请求分配指定的IO内存资源。

      2) check_mem_region()

      检查指定的IO内存资源是否已被占用。

      3) release_mem_region()

      释放指定的IO内存资源。

      其中传给函数的start address参数是内存区的物理地址(以上函数参数表已省略)。

      驱动开发人员可以将内存映射方式的IO端口和外设内存统一看作是IO内存资源。

      ioremap()用来将IO资源的物理地址映射到内核虚地址空间(3GB - 4GB)中,参数addr是指向内核虚地址的指针。

      Linux中可以通过以下辅助函数来访问IO内存资源:

      readb(),readw(),readl(),writeb(),writew(),writel()。

      Linux在kernel/resource.c文件中定义了全局变量ioport_resource和iomem_resource,来分别描述基于IO映射方式的整个IO端口空间和基于内存映射方式的IO内存资源空间(包括IO端口和外设内存)。

    1)关于IO与内存空间:
        在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的,它通过特定的指令in、out来访问。端口号标识了外设的寄存器地址。Intel语法的in、out指令格式为:
        IN 累加器, {端口号│DX}
        OUT {端口号│DX},累加器
        目前,大多数嵌入式微控制器如ARM、PowerPC等中并不提供I/O空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。 
        即便是在X86处理器中,虽然提供了I/O空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的I/O指令。因此,内存空间是必须的,而I/O空间是可选的。

    (2)inb和outb:

    在Linux设备驱动中,宜使用Linux内核提供的函数来访问定位于I/O空间的端口,这些函数包括:
    · 读写字节端口(8位宽)
    unsigned inb(unsigned port); 
    void outb(unsigned char byte, unsigned port); 
    · 读写字端口(16位宽)
    unsigned inw(unsigned port); 
    void outw(unsigned short word, unsigned port); 
    · 读写长字端口(32位宽)
    unsigned inl(unsigned port); 
    void outl(unsigned longword, unsigned port); 
    · 读写一串字节
    void insb(unsigned port, void *addr, unsigned long count); 
    void outsb(unsigned port, void *addr, unsigned long count);
    · insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存;outsb()将addr指向的内存的count个字节连续地写入port开始的端口。
    · 读写一串字
    void insw(unsigned port, void *addr, unsigned long count); 
    void outsw(unsigned port, void *addr, unsigned long count); 
    · 读写一串长字
    void insl(unsigned port, void *addr, unsigned long count); 
    void outsl(unsigned port, void *addr, unsigned long count); 
    上述各函数中I/O端口号port的类型高度依赖于具体的硬件平台,因此,只是写出了unsigned。


    文章出处:飞诺网(www.diybl.com):http://www.diybl.com/course/6_system/linux/linuxjq/20110922/560798.html

    arm ,Linux里面,全部都会做phy->virt的映射。映射方式中的一种是静态映射,ioremap是动态映射。在静态映射之后,仍然可以通过ioremap动态映射,也就是一个IO物理地址可以映射到多个虚拟地址。

    (1)关于IO与内存空间:
         在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的,它通过特定的指令in、out来访问。端口号标识了外设的寄存器地址。Intel语法的in、out指令格式为:
         IN 累加器, {端口号│DX}
         OUT {端口号│DX},累加器
         目前,大多数嵌入式微控制器如ARM、PowerPC等中并不提供I/O空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。 
         即便是在X86处理器中,虽然提供了I/O空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的I/O指令。因此,内存空间是必须的,而I/O空间是可选的。

    (2)inb和outb:

    在Linux设备驱动中,宜使用Linux内核提供的函数来访问定位于I/O空间的端口,这些函数包括:
    · 读写字节端口(8位宽)
    unsigned inb(unsigned port); 
    void outb(unsigned char byte, unsigned port); 
    · 读写字端口(16位宽)
    unsigned inw(unsigned port); 
    void outw(unsigned short word, unsigned port); 
    · 读写长字端口(32位宽)
    unsigned inl(unsigned port); 
    void outl(unsigned longword, unsigned port); 
    · 读写一串字节
    void insb(unsigned port, void *addr, unsigned long count); 
    void outsb(unsigned port, void *addr, unsigned long count);
    · insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存;outsb()将addr指向的内存的count个字节连续地写入port开始的端口。
    · 读写一串字
    void insw(unsigned port, void *addr, unsigned long count); 
    void outsw(unsigned port, void *addr, unsigned long count); 
    · 读写一串长字
    void insl(unsigned port, void *addr, unsigned long count); 
    void outsl(unsigned port, void *addr, unsigned long count); 
    上述各函数中I/O端口号port的类型高度依赖于具体的硬件平台,因此,只是写出了unsigned。

    (3)readb和writeb:
    在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是工程师宜使用Linux内核的如下一组函数来完成设备内存映射的虚拟地址的读写,这些函数包括:
    · 读I/O内存
    unsigned int ioread8(void *addr);
    unsigned int ioread16(void *addr);
    unsigned int ioread32(void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    unsigned readb(address);
    unsigned readw(address);
    unsigned readl(address);
    · 写I/O内存
    void iowrite8(u8 value, void *addr);
    void iowrite16(u16 value, void *addr);
    void iowrite32(u32 value, void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    void writeb(unsigned value, address);
    void writew(unsigned value, address);
    void writel(unsigned value, address);

    (4)把I/O端口映射到“内存空间”:
    void *ioport_map(unsigned long port, unsigned int count);
    通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。当不再需要这种映射时,需要调用下面的函数来撤消:
    void ioport_unmap(void *addr);
    实际上,分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口访问I/O端口。



    http://blog.csdn.net/ce123/article/details/7204458


    一、I/O端口

          端口(port)是接口电路中能被CPU直接访问的寄存器的地址。几乎每一种外设都是通过读写设备上的寄存器来进行的。CPU通过这些地址即端口向接口电路中的寄存器发送命令,读取状态和传送数据。外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连续地编址。

    二、IO内存

           例如,在PC上可以插上一块图形卡,有2MB的存储空间,甚至可能还带有ROM,其中装有可执行代码。

    三、IO端口和IO内存的区分及联系

             这两者如何区分就涉及到硬件知识,X86体系中,具有两个地址空间:IO空间和内存空间,而RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,即内存空间。
    内存空间:内存地址寻址范围,32位操作系统内存空间为2的32次幂,即4G。
    IO空间:X86特有的一个空间,与内存空间彼此独立的地址空间,32位X86有64K的IO空间。

    IO端口:当寄存器或内存位于IO空间时,称为IO端口。一般寄存器也俗称I/O端口,或者说I/O ports,这个I/O端口可以被映射在Memory Space,也可以被映射在I/O Space。

    IO内存:当寄存器或内存位于内存空间时,称为IO内存。

     

    四、外设IO端口物理地址的编址方式

            CPU对外设IO端口物理地址的编址方式有两种:一种是I/O映射方式(I/O-mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于CPU的体系结构。

    1、统一编址

      RISC指令系统的CPU(如,PowerPC、m68k、ARM等)通常只实现一个物理地址空间(RAM)。在这种情况下,外设I/O端口的物理地址就被映射到CPU的单一物理地址空间中,而成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。

           统一编址也称为“I/O内存”方式,外设寄存器位于“内存空间”(很多外设有自己的内存、缓冲区,外设的寄存器和内存统称“I/O空间”)。

    2、独立编址

            而另外一些体系结构的CPU(典型地如X86)则为外设专门实现了一个单独地地址空间,称为“I/O地址空间”或者“I/O端口空间”。这是一个与CPU地RAM物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元(也即I/O端口)。与RAM物理地址空间相比,I/O地址空间通常都比较小,如x86 CPU的I/O空间就只有64KB(0-0xffff)。这是“I/O映射方式”的一个主要缺点。

            独立编址也称为“I/O端口”方式,外设寄存器位于“I/O(地址)空间”。

    3、优缺点

    独立编址主要优点是:
    1)、I/O端口地址不占用存储器空间;使用专门的I/O指令对端口进行操作,I/O指令短,执行速度快。
    2)、并且由于专门I/O指令与存储器访问指令有明显的区别,使程序中I/O操作和存储器操作层次清晰,程序的可读性强。
    3)、同时,由于使用专门的I/O指令访问端口,并且I/O端口地址和存储器地址是分开的,故I/O端口地址和存储器地址可以重叠,而不会相互混淆。 
    4)、译码电路比较简单(因为I/0端口的地址空间一般较小,所用地址线也就较少)。
    其缺点是:只能用专门的I/0指令,访问端口的方法不如访问存储器的方法多。

    统一编址优点:
    1)、由于对I/O设备的访问是使用访问存储器的指令,所以指令类型多,功能齐全,这不仅使访问I/O端口可实现输入/输出操作,而且还可对端口内容进行算术逻辑运算,移位等等;
    2)、另外,能给端口有较大的编址空间,这对大型控制系统和数据通信系统是很有意义的。
    这种方式的缺点是端口占用了存储器的地址空间,使存储器容量减小,另外指令长度比专门I/O指令要长,因而执行速度较慢。

            究竟采用哪一种取决于系统的总体设计。在一个系统中也可以同时使用两种方式,前提是首先要支持I/O独立编址。Intel的x86微处理器都支持I/O 独立编址,因为它们的指令系统中都有I/O指令,并设置了可以区分I/O访问和存储器访问的控制信号引脚。而一些微处理器或单片机,为了减少引脚,从而减 少芯片占用面积,不支持I/O独立编址,只能采用存储器统一编址。

    五、Linux下访问IO端口

               对于某一既定的系统,它要么是独立编址、要么是统一编址,具体采用哪一种则取决于CPU的体系结构。 如,PowerPC、m68k等采用统一编址,而X86等则采用独立编址,存在IO空间的概念。目前,大多数嵌入式微控制器如ARM、PowerPC等并不提供I/O空间,仅有内存空间,可直接用地址、指针访问。但对于Linux内核而言,它可能用于不同的CPU,所以它必须都要考虑这两种方式,于是它采用一种新的方法,将基于I/O映射方式的或内存映射方式的I/O端口通称为“I/O区域”(I/O region),不论你采用哪种方式,都要先申请IO区域:request_resource(),结束时释放它:release_resource()。

    IO region是一种IO资源,因此它可以用resource结构类型来描述。

             访问IO端口有2种途径:I/O映射方式(I/O-mapped)、内存映射方式(Memory-mapped)。前一种途径不映射到内存空间,直接使用 intb()/outb()之类的函数来读写IO端口;后一种MMIO是先把IO端口映射到IO内存(“内存空间”),再使用访问IO内存的函数来访问 IO端口。

    1、I/O映射方式

           直接使用IO端口操作函数:在设备打开或驱动模块被加载时申请IO端口区域,之后使用inb(),outb()等进行端口访问,最后在设备关闭或驱动被卸载时释放IO端口范围。

     in、out、ins和outs汇编语言指令都可以访问I/O端口。内核中包含了以下辅助函数来简化这种访问:

    inb( )、inw( )、inl( )
    分别从I/O端口读取1、2或4个连续字节。后缀“b”、“w”、“l”分别代表一个字节(8位)、一个字(16位)以及一个长整型(32位)。

    inb_p( )、inw_p( )、inl_p( )
    分别从I/O端口读取1、2或4个连续字节,然后执行一条“哑元(dummy,即空指令)”指令使CPU暂停。

    outb( )、outw( )、outl( )
    分别向一个I/O端口写入1、2或4个连续字节。

    outb_p( )、outw_p( )、outl_p( )
    分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“哑元”指令使CPU暂停。

    insb( )、insw( )、insl( )
    分别从I/O端口读入以1、2或4个字节为一组的连续字节序列。字节序列的长度由该函数的参数给出。

    outsb( )、outsw( )、outsl( )
    分别向I/O端口写入以1、2或4个字节为一组的连续字节序列。

    流程如下:

          虽然访问I/O端口非常简单,但是检测哪些I/O端口已经分配给I/O设备可能就不这么简单了,对基于ISA总线的系统来说更是如此。通常,I/O设备驱动程序为了探测硬件设备,需要盲目地向某一I/O端口写入数据;但是,如果其他硬件设备已经使用这个端口,那么系统就会崩溃。为了防止这种情况的发生,内核必须使用“资源”来记录分配给每个硬件设备的I/O端口。资源表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。在这里,资源表示I/O端口地址的一个范围。每个资源对应的信息存放在resource数据结构中:

    [plain]  view plain copy
    1. struct resource {  
    2.          resource_size_t start;// 资源范围的开始  
    3.          resource_size_t end;// 资源范围的结束  
    4.          const char *name; //资源拥有者的名字  
    5.          unsigned long flags;// 各种标志  
    6.          struct resource *parent, *sibling, *child;// 指向资源树中父亲,兄弟和孩子的指针  
    7. };  


              所有的同种资源都插入到一个树型数据结构(父亲、兄弟和孩子)中;例如,表示I/O端口地址范围的所有资源都包括在一个根节点为ioport_resource的树中。节点的孩子被收集在一个链表中,其第一个元素由child指向。sibling字段指向链表中的下一个节点。

            为什么使用树?例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000 到 0xf00f。那么,start字段为0xf000 且end 字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。但是,IDE设备驱动程序需要记住另外的信息,也就是IDE链主盘使用0xf000 到0xf007的子范围,从盘使用0xf008 到0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000 到0xf00f的整个范围对应的资源下。一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。I/O端口资源树(ioport_resource)的根节点跨越了整个I/O地址空间(从端口0到65535)。

    任何设备驱动程序都可以使用下面三个函数,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:

    request_resource( )        //把一个给定范围分配给一个I/O设备。

    allocate_resource( )        //在资源树中寻找一个给定大小和排列方式的可用范围;若存在,将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,可以使用任意的端口号和主板上的内存地址对其进行配置)。

    release_resource( )      //释放以前分配给I/O设备的给定范围。

    内核也为以上函数定义了一些应用于I/O端口的快捷函数:request_region( )分配I/O端口的给定范围,release_region( )释放以前分配给I/O端口的范围。当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。

    2、内存映射方式

             将IO端口映射为内存进行访问,在设备打开或驱动模块被加载时,申请IO端口区域并使用ioport_map()映射到内存,之后使用IO内存的函数进行端口访问,最后,在设备关闭或驱动模块被卸载时释放IO端口并释放映射。

    映射函数的原型为:
    void *ioport_map(unsigned long port, unsigned int count);
    通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。但请注意,在进行映射前,还必须通过request_region( )分配I/O端口。

    当不再需要这种映射时,需要调用下面的函数来撤消:
    void ioport_unmap(void *addr);

    在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是宜使用Linux内核的如下一组函数来完成访问I/O内存:·读I/O内存
    unsigned int ioread8(void *addr);
    unsigned int ioread16(void *addr);
    unsigned int ioread32(void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    unsigned readb(address);
    unsigned readw(address);
    unsigned readl(address);
    ·写I/O内存
    void iowrite8(u8 value, void *addr);
    void iowrite16(u16 value, void *addr);
    void iowrite32(u32 value, void *addr);
    与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
    void writeb(unsigned value, address);
    void writew(unsigned value, address);
    void writel(unsigned value, address);

    流程如下:

    六、Linux下访问IO内存

             IO内存的访问方法是:首先调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间的虚拟地址,之后就可以Linux设备访问编程接口访问这些寄存器了,访问完成后,使用ioremap()对申请的虚拟地址进行释放,并释放release_mem_region()申请的IO内存资源。

    struct resource *requset_mem_region(unsigned long start, unsigned long len,char *name);
       这个函数从内核申请len个内存地址(在3G~4G之间的虚地址),而这里的start为I/O物理地址,name为设备的名称。注意,。如果分配成功,则返回非NULL,否则,返回NULL。
    另外,可以通过/proc/iomem查看系统给各种设备的内存范围。

    要释放所申请的I/O内存,应当使用release_mem_region()函数:
    void release_mem_region(unsigned long start, unsigned long len)

    申请一组I/O内存后, 调用ioremap()函数:
    void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
    其中三个参数的含义为:
    phys_addr:与requset_mem_region函数中参数start相同的I/O物理地址;
    size:要映射的空间的大小;
    flags:要映射的IO空间的和权限有关的标志;

    功能:将一个I/O地址空间映射到内核的虚拟地址空间上(通过release_mem_region()申请到的)

    流程如下:

    六、ioremap和ioport_map

    下面具体看一下ioport_map和ioport_umap的源码:

    [plain]  view plain copy
    1. void __iomem *ioport_map(unsigned long port, unsigned int nr)  
    2. {  
    3.     if (port > PIO_MASK)  
    4.         return NULL;  
    5.     return (void __iomem *) (unsigned long) (port + PIO_OFFSET);  
    6. }  
    7.   
    8. void ioport_unmap(void __iomem *addr)  
    9. {  
    10.     /* Nothing to do */  
    11. }  

              ioport_map仅仅是将port加上PIO_OFFSET(64k),而ioport_unmap则什么都不做。这样portio的64k空间就被映射到虚拟地址的64k~128k之间,而ioremap返回的虚拟地址则肯定在3G之上。ioport_map函数的目的是试图提供与ioremap一致的虚拟地址空间。分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口ioread8/iowrite8(......)访问I/O端口。
              最后来看一下ioread8的源码,其实现也就是对虚拟地址进行了判断,以区分IO端口和IO内存,然后分别使用inb/outb和readb/writeb来读写。

    [plain]  view plain copy
    1. unsigned int fastcall ioread8(void __iomem *addr)  
    2. {  
    3.     IO_COND(addr, return inb(port), return readb(addr));  
    4. }  
    5.   
    6. #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)  
    7. #define IO_COND(addr, is_pio, is_mmio) do { \  
    8.     unsigned long port = (unsigned long __force)addr; \  
    9.         if (port < PIO_RESERVED) { \  
    10.             VERIFY_PIO(port); \  
    11.             port &= PIO_MASK; \  
    12.             is_pio; \  
    13.         } else { \  
    14.             is_mmio; \  
    15.         } \  
    16. } while (0)  
    17.   
    18. 展开:  
    19. unsigned int fastcall ioread8(void __iomem *addr)  
    20. {  
    21.     unsigned long port = (unsigned long __force)addr;  
    22.     if( port < 0x40000UL ) {  
    23.         BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );  
    24.         port &= PIO_MASK;  
    25.         return inb(port);  
    26.     }else{  
    27.         return readb(addr);  
    28.     }  
    29. }  

    七、总结

             外设IO寄存器地址独立编址的CPU,这时应该称外设IO寄存器为IO端口,访问IO寄存器可通过ioport_map将其映射到虚拟地址空间,但实际上这是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了可以使用和IO内存一样的接口访问IO寄存器;也可以直接使用in/out指令访问IO寄存器。

              例如:Intel x86平台普通使用了名为内存映射(MMIO)的技术,该技术是PCI规范的一部分,IO设备端口被映射到内存空间,映射后,CPU访问IO端口就如同访 问内存一样。

              外设IO寄存器地址统一编址的CPU,这时应该称外设IO寄存器为IO内存,访问IO寄存器可通过ioremap将其映射到虚拟地址空间,然后再使用read/write接口访问。



    展开全文
  • Linux系统对IO端口和IO内存的 管理 Linux系统对IO端口和IO内存的管理 一、I/O端口 端口(port)是接口电路中能被CPU直接访问的寄存器的地址。几乎每一种外设都是通过读写设备上的寄存器来进行的。CPU通过这些地址即...
  • IO端口&IO内存

    2018-03-14 11:39:43
    IO端口和IO内存的区别及分别...)[+]IO端口和IO内存的区别及分别使用的函数接口 每外设都是通过读写其寄存器来控制的。外设寄存器也称为I/O端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。根据访问外...
  • 原标题:单片机小白学步(21) IO口:基本的LED和按键操作本篇我们先介绍最基础的几个IO口操作。学完本篇,大家要能自己独立完成下面几个实验。1、点亮2、一个按键控制LED,按下灯亮,释放灯灭3、两个按键控制一个LED...
  • IO端口与IO内存管理

    2015-09-05 23:43:16
    一、I/O端口    端口(port)是接口电路中能被CPU直接...外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一外设的寄存器通常被连续地编址。   二、IO内存    例如,在P
  • Linux系统对IO端口和IO内存的管理
  • 对于40引脚双列直插51单片机各个引脚功能情况分析。P0端口一默认是开漏准双向IO口,没有输出没有驱动能力,要做逻辑输出要在外部接上拉电阻。...P3端口除了普通IO端口使用,还有特殊的功能,有特殊的寄存器配合...
  • IO端口二IO内存三IO端口和IO内存的区分及联系外设IO端口物理地址的编址方式 统一编址独立编址优缺点 五Linux下访问IO端口 IO映射方式内存映射方式 六Linux下访问IO内存六ioremap和ioport_map七总结 ...
  • io端口与ion内存

    2015-12-03 21:35:55
    linux中的 IO端口映射和IO内存映射 (一)地址的概念 1)物理地址:CPU地址总线传来的地址,由硬件电路控制其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上 (如显存、BI
  • io端口和管脚 如何访问8051单片机中的端口和引脚 (How to access Ports and Pins in 8051 Microcontroller) The 8051 microcontrollers have 8-bit ports. All the ports are bidirectional in nature, which means ...
  • 51单片机io

    千次阅读 多人点赞 2017-08-06 13:49:28
    今天进入正式章节:单片机io口。首先要弄懂io的两大功能,第一:普通io,第二:第二功能,如:串口、AD转换、外部中断口。当使用引脚的第二功能时不能使用为普通io口。 那么io有哪几类呢?这里说的是51单片机总的...
  • <摘录>io端口和io内存

    2015-03-07 14:20:00
    linux中的 IO端口映射和IO内存映射 (一)地址的概念 1)物理地址:CPU地址总线传来的地址,由硬件电路控制其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上 (如显存、BIOS...
  • IO / Linux系统对IO端口和IO内存的管理

    千次阅读 2012-03-23 17:05:49
    一、I/O端口  端口(port)是接口电路中能...外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一外设的寄存器通常被连续地编址。   二、IO内存  例如,在PC上可以插上
  • 一、I/O端口 端口(port)是接口电路中能被CPU直接...外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一外设的寄存器通常被连续地编址。 二、IO内存 例如,在PC上可...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,237
精华内容 1,294
关键字:

单片机的四个io端口