首页 > 编程知识 正文

X86下 IO端口和IO内存外设IO访问以pci设备为例

时间:2023-05-03 23:56:14 阅读:202439 作者:2387

首先要搞清楚一些基本概念: 1、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)。

1)、I/O端口

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

2)、IO内存

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

3)、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内存。

具体分析见这里:https://www.cnblogs.com/reality-soul/p/6126376.html

2、CPU对IO端口的编址方式2有两种:

几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,两种方式如下:

 

(1)I/O映射方式(I/O-mapped)
典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。
(2)内存映射方式(Memory-mapped)
RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。
但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。
 一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。我们知道在linux内存管理中物理内存有严格的分配机制,当物理内存被分配完,那么其他的IO外设地址该如何映射呢?CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,原型如下:

3、具体ioremap函数分析

void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
入口: phys_addr:要映射的起始的IO地址;
size:要映射的空间的大小;
flags:要映射的IO空间的和权限有关的标志;
功能: 将一个IO地址空间映射到内核的虚拟地址空间上去,便于访问;
实现:对要映射的IO地址空间进行判断,低PCI/ISA地址不需要重新映射,也不允许用户将IO地址空间映射到正在使用的RAM中,最后申请一 个 vm_area_struct结构,调用remap_area_pages填写页表,若填写过程不成功则释放申请的vm_area_struct
意义:比如isa设备和pci设备,或者是fb,硬件的跳线或者是物理连接方式决定了硬件上的内存影射到的cpu物理地址。
在内核访问这些地址必须分配给这段内存以虚拟地址,这正是__ioremap的意义所在 ,需要注意的是,物理内存已经"存在"了,无需alloc page给这段地址了.
为了使软件访问I/O内存,必须为设备分配虚拟地址.这就是ioremap的工作.这个函数专门用来为I/O内存区域分配虚拟地址(空间).对于直接映射的I/O地址ioremap不做任何事情。有了ioremap(和iounmap),设备就可以访问任何I/O内存空间,不论它是否直接映射到虚拟地址空间.但是,这些地址永远不能直接使用(指物理地址),而要用readb这种函数。

使用I/O内存首先要申请,然后才能映射,使用I/O端口首先要申请,或者叫请求,对于I/O端口的请求意思是让内核知道你要访问这个端口,这样内核知道了以后它就不会再让别人也访问这个端口了.毕竟这个世界僧多粥少啊.申请I/O端口的函数是request_region, 申请I/O内存的函数是request_mem_region。request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。重要的还是ioremap函数,ioremap主要是检查传入地址的合法性,建立页表(包括访问权限),完成物理地址到虚拟地址的转换

使用方法:

内核中的使用,往往是为某个设备预留一块内存,当使用的时候需要在board中定义这个设备的内存resource。通过 platform_get_resource获得设备的起始地址后,可以对其进行request_mem_region和ioremap等操作,以便应用程序对其进行操作。

在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。如在x86平台上,读写I/O的函数如下所示:

#definereadb(addr) (*(volatile unsigned char *) __io_virt(addr))#define readw(addr) (*(volatile unsigned short *) __io_virt(addr))#define readl(addr) (*(volatile unsigned int *) __io_virt(addr))#definewriteb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b))#define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b))#define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b))#definememset_io(a,b,c) memset(__io_virt(a),(b),(c))#define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))#define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))

最后,我们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问,这样就实现了虚拟地址到物理地址的映射与操作。

系统如何访问PCI设备

我们知道CPU和网卡是通过PCI总线相连的,CPU可以直接访问系统内存(虚拟地址),也可以通过映射间接访问总线地址,那CPU怎么访问网卡的存储空间呢?

每个网卡都有自己的存储空间,这些空间的卡上地址(在网卡上的地址)本质上是局部的,所以都从0开始,它们不与总线直接相连,在把网卡插上总线并加电之初,从总线上还访问不到这些空间

系统初始化时扫描PCI总线上的各个PCI设备(包括网卡),为这些设备分配总线地址,并建立起其卡上地址和总线地址的映射,那映射是怎么建立起来的?

每个PCI设备上都有用来建立映射的配置寄存器组(配置空间),系统初始化时通过这组寄存器来为设备"配置"总线地址,那CPU怎么访问这组寄存器呢?这就又回到原点了

PCI标准规定配置寄存器组最大256 byte,其中开头64 byte是标准的(对每个PCI设备都一样),所有PCI设备的配置寄存器组都使用相同的地址(卡上地址或偏移量)

系统在IO地址空间预留了八个字节(0xCF8~0xCFF),其中前四个字节做地址寄存器,后四个字节做数据寄存器,当CPU访问某个设备的某个配置寄存器时,首先通过I/O命令向地址寄存器写入目标地址(包括总线号、设备号、功能号、寄存器地址的综合地址),然后通过I/O命令读写数据寄存器

综合地址的结构如下图所示,其中寄存器地址的高6位用于寻址最大64 word(256 byte)的配置寄存器组

 

如下图所示,一个256B代表一个配置寄存器组,最多有2^16个配置寄存器组(最大有16MB)

写入综合地址后,从0号总线开始,每个PCI桥将综合地址中的总线号和自己的总线号相比,若符合,根据设备号+功能号寻找设备;若不符合,将综合地址传递给下一级总线的PCI桥继续寻找,直到找到设备,最后根据8位寄存器地址找到配置寄存器,此时通过I/O命令读写数据寄存器就可以读写配置寄存器了

系统初始化时扫描PCI总线上的各个PCI设备(包括网卡),为这些设备分配总线地址,设备的每个BAR对应设备的一段存储空间,系统通过将总线地址写入BAR就建立了存储空间的卡上地址和总线地址的映射

设备驱动程序调用pci_ioremap_bar()将写入BAR的总线地址(保存在resource数组中)映射到系统内存的虚拟地址,之后CPU就可以通过虚拟地址访问PCI设备的存储空间,而不用再通过IO命令了

HD-driver中关于声卡resource的访问: err = pci_request_regions(pci, "ICH HD audio"); if (err < 0) return err; chip->region_requested = 1; chip->addr = pci_resource_start(pci, 0); chip->remap_addr = pci_ioremap_bar(pci, 0);

先在内存将要使用的地址空间,然后获得声卡的基地址,然后将bar映射到内存中去,这样就可以访问这个声卡的PCI 内存区域了。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。