打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
与硬件通讯---IO端口
一、简述
每个外部设备都是通过读写它的寄存器来控制的;大部分时间,一个设备有好几个寄存器,并且都在连续的地址空间内存取它们:或者是在内存地址空间,或者是在IO地址空间;
在硬件级别上,内存区和IO区在概念上没有区别:它们都是通过在地址总线和控制总线上发出电信号来存取(即:读写信号),并且从数据总线上读取数据或者把数据写到数据总线上;
但是有一些CPU制造商在他们的芯片上实现了一个单个地址空间,有人为人外部设备不同于内存,应该有一个独立的地址空间;有一些处理器就有独立的读写电路给IO端口和特殊的CPU指令来存取端口;因为外部设备被建立来适应一个外部总线,并且大部分流行的IO总线成型在个人计算机上,即便是那些没有独立地址空间给IO端口的处理器,也必须在存取一些特殊设备时伪装读写端口,常常利用外部芯片组或者CPU核的额外电路;
由于同样的理由,Linux在它所运行的所有计算机平台上都实现了IO端口的概念,甚至在那些CPU实现一个独立地址空间的平台上;端口存取的实现有时依赖于特殊的主机制造和型号(因为不同的型号使用不同的芯片组来影射总线传送到内存地址空间);
即便是外设总线有一个独立的地址空间给IO端口,也不是所有的设备都影射它们的寄存器到IO端口;虽然对于ISA外设板使用IO端口是普遍的,但是PCI设备还是仍然影射寄存器到一个内存地址空间中,这种IO内存方法通常是首选的,因为它不需要是同特殊的目的的处理器指令,CPU核存取内存更有效,并且编译器当存取内存时有更多自由在寄存器分配和寻址模式的选择上;
二、IO寄存器和常规内存
不管硬件寄存器与内存之间的强相似性,存取IO寄存器时必须要小心避免被CPU(或编译器)优化,它可能修改希望的IO行为;
IO寄存器与内存的主要区别是:IO寄存器操作有边际效果,而内存操作没有;一个内存写操作的唯一效果就是存储一个值到一个位置,并且一个内存读操作是返回最近写到那里的值;因为内存存取速度对CPU性能至关重要,这种无边际效果的情况已经被多种方式优化:值被缓存,并且读写指令被重新编排;
编译器能够缓存数据值到CPU寄存器而不是写到内存,即便是存储它们,读写操作都能够在缓冲内存中进行,而不接触物理内存;指令重编排操作也可能在编译器级别和硬件级别上都发生:常常一个指令序列能够执行得更快,如果它以不同于在程序文本中出现的顺序来执行;例如:为避免在RISC流水线中的互锁;在CISC处理器中,要花费相当数量时间的操作能够和其它的并发执行,更快;
编译器或CPU可能会重新编排你的操作顺序,结果可能是出现一些奇怪的错误而难于调试;因此,一个驱动程序必须确保没有进行缓冲并且在存取寄存器时没有发生读或写操作的重编排;
硬件缓冲的问题易于面对:底层的硬件已经配置(或者自动地或者通过Linux初始化代码)成禁止任何硬件缓冲,当存取IO区时(不管是内存区还是端口区);
对编译器优化和硬件重编排的解决方法是放置一个内存屏障在必须以一个特殊顺序对硬件(或者另一个处理器)可见的操作之间;
Linux提供了4个宏来应对可能的排序需要:
1、
#include <linux/kernel.h>
void barrier(void);
该函数告诉编译器插入一个内存屏障,但是对硬件没有影响;编译的代码将所有的当前改变的并且驻留在CPU寄存器中的值存储到内存中,并且在以后需要的时候再重新读取它们;对屏障的调用会阻止编译器跨越屏障的优化,而留给硬件自由做它的重编排;
2、
#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
这些函数用于在编译的指令流中插入硬件内存屏障;它们的实际实现是平台相关的;一个rmb(read memory barrier)保证任何出现于屏障之前的读操作都在执行后续的读操作之前完成;wmb保证了写操作的顺序;mb不仅保证了读操作的顺序,也保证了写操作的顺序;每一个指令都是一个屏障超集;
read_barrier_depends是读屏障的一个特殊的、弱一些的形式;rmb指令阻止所有跨越屏障的读操作的重编排,read_barrier_depends指令只阻止依赖来自其它读的数据的读操作的重编排;区别是微小的,并且它并不在所有体系中存在.除非你确切地理解做什么,并且有理由相信,一个完整的读屏障确实是一个过渡地性能开销,你可能应当坚持使用rmb;
3、
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
这些版本的插入屏障指令仅用于为SMP架构的CPU内核系统编译时插入硬件屏障;否则,它们都扩展为一个简单的屏障调用;
在一个驱动程序中,一个典型的内存屏障的用法可能有如下这样的形式:
writel(dev->registers.addr,io_destination_address);
writel(dev->registers.size,io_size);
writel(dev->registers.operation,DEV_READ);
wmb();
writel(dev->registers.control,DEV_GO);
在这种情况,是重要的,确保所有的控制一个特殊操作的设备寄存器在告诉它开始前已被正确设置,内存屏障强制写操作以需要的顺序完成;
因为内存屏障影响性能,所以它们只用在确实需要的地方;不同类型的屏障也有不同的性能,因此值得使用最特定的可能类型;例如:x86体系上,wmb()什么都不做,因为写到处理器外不被重编排,但是读操作被重编排,因此,mb()比wmb()慢;
注意:大部分处理同步的内核原语如同内存屏障一样都是函数,还要注意的是,有一些外设总线(如PCI总线)有它们自己的缓冲问题;
在合适的地方,<asm/system.h>定义这些宏来使用体系特定的指令来很快完成任务;
三、使用IO端口
IO端口是驱动程序用来与设备通讯的手段之一;
1、IO端口分配:
如果要使用IO端口,那就必须要对IO端口有使用权限,这些权限都需要向内核申请;内核提供了一个注册接口以允许驱动程序来声明它所需要使用的IO端口;
#include <linux/ioport.h>
struct resource* request_region(unsigned long first, unsigned long n, const char* name);
这个函数用于注册驱动程序需要使用的端口;它告诉内核,要使用n个端口,从first开始;name参数应当是使用这些IO端口的那个设备的名字;如果申请分配IO端口成功,则返回值是非NULL,这时可以正常使用所需要的IO端口;如果申请分配IO端口失败,则返回值是NULL,这时将无法使用所需要的IO端口;所有的IO端口分配都显示在/proc/ioports中;如果不能分配一组需要的IO端口,就可以到这里来看看IO端口占用情况;
2、IO端口释放:
当用完一组IO端口时(如:模块卸载时),应当把这组IO端口返回给系统;
void release_region(unsigned long first, unsigned long n);
3、校验IO端口有效性:
有时,在使用一组IO端口之前还可以先校验一下这组IO端口是否可用:
int check_region(unsigned long first, unsigned long n);
如果返回值是一个负的错误码,则表示这组IO端口不可用;这个函数是不推荐使用的,因为它的返回值不能保证是否一个分配会成功;检查动作和后来的分配动作不是一个原子操作;
4、操作IO端口:
驱动程序请求了硬件在活动期间需要使用的IO端口范围之后,就可以对这组IO端口读/写数据了;为此,大部分硬件都区分8-bit、16-bit、32-bit的端口,而且无法混合,必须单独使用;所以,一个C程序就必须调用不同的函数来存取不同大小的IO端口;只支持唯一内存映射IO寄存器的计算机体系伪装IO端口,通过重新映射端口地址到内存地址,并且向内核驱动程序隐藏了实现细节,以便于移植;
1):单字节IO
unsigned char inb(unsigned long port);              //读
void outb(unsigned char byte, unsigned long port);  //写
读写字节端口(8bit);port参数定义为unsigned long类型的,在某些平台还有可能是unsigned short类型的;inb()的返回值类型也是依赖于具体平台体系的;
2):双字节IO
unsigned short inw(unsigned long port);             //读
void outw(unsigned short word, unsigned long port); //写
读写16-bit端口;在位S390平台编译时不可用,它只支持字节IO;
3):四字节IO
unsigned long inl(unsigned long port);              //读
void outl(unsigned long dword, unsigned long port); //写
读写四字节IO端口;dword可声明为unsigned long或unsigned int,依据平台决定;
注意:没有定义64-bit(8字节)端口;甚至在64-bit体系中,端口地址空间使用一个32-bit(最大)的数据通路;
4):字串操作
底层的字串操作一般都是通过紧凑的汇编循环指令实现的;
A:单字节字串IO
void insb(unsigned long port, void* addr, unsigned long count);  //读
void outsb(unsigned long port, void* addr, unsigned long count); //写
从内存地址addr处开始读/写count个字节;数据读自/写入单个port端口;
B:双字节字串IO
void insw(unsigned long port, void* addr, unsigned long count);  //读
void outsw(unsigned long port, void* addr, unsigned long count); //写
读/写一个16-bit值到一个16-bit端口port;
C:四字节字串IO
void insl(unsigned long port, void* addr, unsigned long count);  //读
void outsl(unsigned long port, void* addr, unsigned long count); //写
读/写一个32-bit值到一个32-bit端口port;
注意:当使用字串函数进行端口IO时,它们会移动一个整齐的字节流到或自端口;当端口与主机系统有不同的字节对齐规则时,结果可能是令人惊讶的;使用inw()读取一个端口交换这些字节,如果需要,来使读取的值匹配主机字节序;字串函数相反,不进行这个交换;
5、暂停IO:
当处理器试图太快地传送数据到或自总线、当处理器对于外设总线被过度滴锁定时,就有可能引起一些问题,并且当外设太慢时会表现出来,或者说当处理器传输数据的速度与外设传输数据的速度相差太大时,就有可能出现一些问题;解决方法是:如果是跟随在另一条指令后面的话,就在每一条IO指令后面插入一个小的延时,以让处理器暂停一下IO;在x86上,这个暂停是通过进行一个outb指令到端口0x80实现的,或者是通过忙等待;详细的实现在平台的asm子目录下的io.h文件中;
如果设备丢失一些数据,或者是担心设备丢失一些数据,那就可以使用暂停函数代替;暂停函数的名字已_p结尾;它们称为inb_p、outb_p、inw_p、oute_p等等;这些函数定义给大部分被支持的体系,尽管它们常常扩展为与非暂停IO同样的代码,因为没有必要额外暂停,如果体系使用一个合理的现代外设总线;
6、平台依赖性:
IO指令的特性是:高度依赖处理器的;ARM处理器上,端口是内存映射的,并且支持所有函数,字串函数用C实现,端口是unsigned int类型;MIPS/MIPS64处理器上,端口支持所有的函数,字串操作使用紧凑汇编循环来实现,因为处理器缺乏机器级别的字串IO,端口是内存映射的,是unsigned long类型;SPARC/SPARC64处理器上,端口是内存映射的,是unsigned long类型;x86_64处理器上,端口是unsigned short类型;
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Linux内核开发之内存与I/O访问(二)
《Linux设备驱动程序》(十五)硬件通信IO设备读写寄存器的过程
ioremap和ioremap
IO端口和IO内存的区别及分别使用的函数接口
linux中的IO端口映射和IO内存映射
Linux设备驱动之I/O端口与I/O内存
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服