访问外部设备寄存器的方法

版权声明:本文为博主原创文章,任何组织或者个人可以在任何媒介上发表或转载我的文章、图片等.且转载后必须注明出处和邮箱,博客地址(https://blog.csdn.net/u011011827),本人邮箱([email protected]) https://blog.csdn.net/u011011827/article/details/76444340
首先从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内存”方式,外设寄存器位于“内存空间”,而内存空间的布局在数据手册中已经确定.
  2/独立编址 
    内存和IO空间 独立编址 就是说内存一部分的地址和I/O地址是重叠的  
    操作外设和内存的汇编指令不同,指令一般都是CISC,soc上多一个外设就要多几个汇编指令(用来操作soc中新添加的外设,这就是soc的升级) 
    独立编址也称为“I/O端口”方式,外设寄存器位于“I/O(地址)空间”
究竟采用哪一种取决于系统的总体设计。在一个系统中也可以同时使用两种方式,也即是说部分设备 独立编址,部分设备统一编址.

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端口
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内存的区别及分别使用的函数接口

统一编址&独立编址

猜你喜欢

转载自blog.csdn.net/u011011827/article/details/76444340