访问外部设备寄存器的方法:ioport与iomem

前言

x86 有 I/O端口(有load store 指令)与I/O内存(没有inb outb 指令) 的概念
	x86 可以只有 IO内存
	x86 可以 有 两个 : IO内存 和 IO端口
arm 只有 IO内存(有load store 指令)的概念,没有I/O端口(没有inb outb 指令) 的概念
------------------------------
x86-linux 有 I/O端口与I/O内存的概念
	iowrite32(xx,xx)  函数 中调用了store 指令
	outl(xx,xx) 函数中调用了 iol 指令
arm-linux 有 IO端口的概念,有I/O内存的概念
	iowrite32(xx,xx)  函数 中调用了 store 指令
	outl(xx,xx) 函数中调用了 iowrite32(xx,xx),最终还是调用了 store 指令 // 可以这么理解,实际上直接调用了 store 指令
首先从IO讲起,关于IO 有很多概念:IO端口 IO接口 IO内存

而我们一般放在一块讨论的是 IO端口与IO接口  IO端口与IO内存, 这两个话题中的IO端口不是一个东西
  
  
而且我们讨论的外设寄存器 是 片上外设寄存器,IO内存涉及到 类ram接口的存储器和其他类ram接口设备
而一般来说其他片外的外设如果和soc不是以类ram接口相连
而是以其他总线方式(I2C SPI MDIO)相连
则是无法通过 IO端口或者IO内存的方式访问的, 一般通过(I2C SPI MDIO)总线方式访问这些片外外设里面的寄存器(注意:片外设备中也集成有存储模块,而我们把一个单元叫做一个寄存器,这里的寄存器不同于soc中的寄存器)

I/O端口与I/O接口

//这是硬件上的概念
I/O接口 : 通常把介于主机和外设之间的一种缓冲电路称为I/O接口电路,简称I/O接口
I/O端口 : CPU与外设进行信息交换时,各类信息通过I/O接口存入不同的寄存器中.这些寄存器被称作I/O端口。

若干端口加上相应的控制电路才构成接口
I/O端口分为 控制寄存器、状态寄存器和数据寄存器

I/O端口与I/O内存

编址指的是访问外设寄存器的方式,相对访问内存来说的.
访问外设寄存器的方式,分为两种(根据外设编址分类)
  1/统一编址 
    内存和IO空间(外设有自己的内存、缓冲区,外设的寄存器和内存)统一编址在总线上拿东西的.
    操作外设和内存的汇编指令相同,指令一般都是RISC 
    统一编址也称为“I/O内存”方式,外设寄存器位于“内存空间”,而内存空间的布局在数据手册中已经确定.
    总之,统一编址就是将所有设备寄存器,内存空间都编到一起(例如一个空间0-4G),用load store 相关指令访问,该空间叫iomem
  2/独立编址 
    内存和IO空间 独立编址 就是说内存一部分的地址和I/O地址是重叠的  
    操作外设和内存的汇编指令不同,指令一般都是CISC,soc上多一个外设就要多几个汇编指令(用来操作soc中新添加的外设,这就是soc的升级) 
    独立编址也称为“I/O端口”方式,外设寄存器位于“I/O(地址)空间”
    总之,独立编址就是将设备寄存器,内存空间不编到一起
    	内存空间(例如一个空间0-4G),用load store 相关指令访问,该空间叫iomem
    	设备控制器空间(例如一个空间0-4G),用 in out 相关指令访问,该空间叫ioport
究竟采用哪一种取决于系统的总体设计。在一个系统中也可以同时使用两种方式,也即是说部分设备 独立编址,部分设备统一编址.

Intel的x86微处理器支持I/O 独立编址,因为它们的指令系统中都有I/O指令,并设置了可以区分I/O访问和存储器访问的控制信号引脚。

一些微处理器或单片机,为了减少引脚,减少芯片占用面积,不支持I/O独立编址,只能采用存储器统一编址,例如arm处理器.

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 内存的情况进行了优化。
访问I/O内存的流程是:
 1/ request_mem_region()
 //这个函数的实现和 request_region 一样,目的一样
 //在访问I/O内存之前,分配I/O内存并不是唯一要求的步骤,你还必须保证内核可存取该I/O内存。访问I/O内存并不只是简单解引用指针,在许多体系中,I/O 内存无法以这种方式直接存取。因此,还必须通过ioremap 函数设置一个映射。
 2/	ioremap() 
 //把设备所处的物理地址映射到虚拟地址 
 //映射之后就可以通过指针来访问这些地址,但是最好不用,最好用linux内核的一组函数来读写,
 //ioremap用于将I/O内存区映射到虚拟地址。参数phys_addr为要映射的I/O内存起始地址,参数size为要映射的I/O内存的大小,返回值为被映射到的虚拟地址
 3/	ioread8()/iowrite16()/ioread8_rep()/iowrite8_rep() 
 //用完了之后要这么做
 4/iounmap()
 5/release_mem_region() 

I/O端口(独立编址)

  • I/O映射方式
通过 I/O映射方式 访问I/O端口的流程是
1/ request_region
	//不映射到内存空间,直接使用 intb()/outb()之类的函数来读写IO端口
	//只是向系统表示,这块空间我用了,每个模块都调用 request_region ,就不会出现 多个模块 outb 同一个io 空间的问题!!!
2/ inb/outb/inw/outw/inl/outl
3/ release_region
  • 内存映射方式
为什么要使用内存映射方式?内存映射方式的重点是什么?
这里的内存映射方式的目的是为了封装IO端口 ,操作起来像IO内存一样操作,即用  IO内存的api ioread8()/iowrite16()/ioread8_rep()/iowrite8_rep() 操作IO端口
关键函数是 ioport_map ,这个函数实现了封装.封装之后,就可以用IO内存的api操作IO端口 
ioport_map 实现了把IO端口映射到IO内存(“内存空间”)
如果不用这个函数封装,那么就要用 inb/outb/inw/outw/inl/outl 操作,这样 IO端口 和 IO内存 的接口就不一样了.


访问 内存映射方式 访问I/O端口的流程是
1/ request_region
//#define request_region(start,n,name)        __request_region(&ioport_resource, (start), (n), (name), 0)

/**
 * __request_region - create a new busy resource region
 * @parent: parent resource descriptor
 * @start: resource start address
 * @n: resource region size
 * @name: reserving caller's ID string
 * @flags: IO resource flags
 */
//struct resource * __request_region(struct resource *parent,
//                   resource_size_t start, resource_size_t n,
//                   const char *name, int flags)
//其实是可以直接映射的,但是会出现一个问题,这个端口如果已经被映射了,我就不应该再映射这个端口.所以为了实现这个东西,就做了一个函数 request_region ,这个函数实现了对 端口映射到内存 的同步
2/ ioport_map
//void __iomem *ioport_map(unsigned long port, unsigned int nr)
//可以把port开始的count个连续的IO端口映射为一段“内存空间”,然后就可以在其返回的地址是像访问IO内存一样访问这些IO端口
3/ 用IO内存的api ioread8()/iowrite16()/ioread8_rep()/iowrite8_rep() 
//大部分硬件会将8位、16位和32位端口区分开,无法像访问内存那样混淆使用。驱动程序必须调用不同的函数来访问不同大小的端口。
//仅支持单地址空间的计算机体系通过将I/O端口地址重新映射到内存地址来伪装端口I/O 。为了提高移植性,内核对驱动隐藏了这些细节。
4/ ioport_unmap
5/ release_region
//用完I/O端口后(可能在模块卸载时),应当调用release_region将I/O端口返还给系统。参数start和n应与之前传递给request_region一致 
//可以用 check_region 来检查某些端口是否被占用

注意:

  • 本文中的函数的参数如果用到了地址,除了下面的函数,其他用到的地址都是虚拟地址

  void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

  参数中用到的是物理地址,返回的是虚拟地址
    
  而这些虚拟地址的获得有两种方案
  1/ioremap
  2/.h文件中的地址
  • ioread8
//ioread8 在 arm 中用的是
#define ioread8(p)  ({ unsigned int __v = __raw_readb(p); __iormb(); __v; })
static inline u8 __raw_readb(const volatile void __iomem *addr)
{
    u8 val;
    asm volatile("ldrb %1, %0"
             : "+Qo" (*(volatile u8 __force *)addr),
               "=r" (val));
    return val;
}

参考资料

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

统一编址&独立编址

  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值