Linux设备驱动程序与硬件通信.docx
《Linux设备驱动程序与硬件通信.docx》由会员分享,可在线阅读,更多相关《Linux设备驱动程序与硬件通信.docx(15页珍藏版)》请在冰豆网上搜索。
![Linux设备驱动程序与硬件通信.docx](https://file1.bdocx.com/fileroot1/2023-1/24/b4db36d7-44d8-42ab-b096-06bba3e16492/b4db36d7-44d8-42ab-b096-06bba3e164921.gif)
Linux设备驱动程序与硬件通信
Linux设备驱动程序--与硬件通信
I/O端口和I/O内存
每种外设都是通过读写寄存器来进行控制。
在硬件层,内存区和I/O区域没有概念上的区别:
它们都是通过向在地址总线和控制总线发出电平信号来进行访问,再通过数据总线读写数据。
因为外设要与I\O总线匹配,而大部分流行的I/O总线是基于个人计算机模型(主要是x86家族:
它为读和写I/O端口提供了独立的线路和特殊的CPU指令),所以即便那些没有单独I/O端口地址空间的处理器,在访问外设时也要模拟成读写I\O端口。
这一功能通常由外围芯片组(PC中的南北桥)或CPU中的附加电路实现(嵌入式中的方法)。
Linux在所有的计算机平台上实现了I/O端口。
但不是所有的设备都将寄存器映射到
I/O端口。
虽然ISA设备普遍使用I/O端口,但大部分PCI设备则把寄存器映射到某个内存地址区,这种I/O
内存方法通常是首选的。
因为它无需使用特殊的处理器指令,CPU
核访问内存更有效率,且编译器在访问内存时在寄存器分配和寻址模式的选择上有更多自由。
I/O寄存器和常规内存
在进入这部分学习的时候,首先要理解一个概念:
side
effect,书中译为边际效应,第二版译为副作用。
我觉得不管它是怎么被翻译的,都不可能精准表达原作者的意思,所以我个人认为记住side
effect就好。
下面来讲讲sideeffect的含义。
我先贴出两个网上已有的两种说法(在这里谢谢两位高人的分享):
第一种说法:
3.sideeffect(译为边际效应或副作用):
是指读取某个地址时可能导致该地址内容发生变化,比如,有些设备的中断状态寄存器只要一读取,便自动清零。
I/O寄存器的操作具有sideeffect,因此,不能对其操作不能使用cpu缓存。
原文网址:
第二种说法:
说一下我的理解:
I/O端口与实际外部设备相关联,通过访问I/O端口控制外部设备,“边际效应”是指控制设备(读取或写入)生效,访问I/O口的
主要目的就是边际效应,不像访问普通的内存,只是在一个位置存储或读取一个数值,没有别的含义了。
我是基于ARM平台理解的,在《linux设备驱动程
序》第二版中的说法是“副作用”,不是“边际效应”。
原文网址:
结合以上两种说法和自己看《Linux设备驱动程序(第3版)》的理解,我个人认为可以这样解释:
sideeffect
是指:
访问I/O寄存器时,不仅仅会像访问普通内存一样影响存储单元的值,更重要的是它可能改变CPU的I/O端口电平、输出时序或CPU对I/O端口电
平的反应等等,从而实现CPU的控制功能。
CPU在电路中的意义就是实现其sideeffect。
I/O寄存器和RAM的主要不同就是I/O寄存器操作有sideeffect,而内存操作没有。
因为存储单元的访问速度对CPU性能至关重要,编译器会对源代码进行优化,主要是:
使用高速缓存保存数值和重新编排读/写指令顺序。
但对I/O寄存器操作来说,这些优化可能造成致命错误。
因此,驱动程序必须确保在操作I/O寄存器时,不使用高速缓存,且不能重新编排读/写指令顺序。
解决方法:
硬件缓存问题:
只要把底层硬件配置(自动地或者通过Linux初始化代码)成当访问I/O区域时(不管内存还是端口)禁止硬件缓存即可。
硬件指令重新排序问题:
在硬件(或其他处理器)必须以一个特定顺序执行的操作之间设置内存屏障(memorybarrier)。
Linux提供以下宏来解决所有可能的排序问题:
#includelinux/kernel.h>
voidbarrier(void)/*告知编译器插入一个内存屏障但是对硬件没有影响。
编译后的代码会将当前CPU寄存器中所有修改过的数值保存到内存中,并当需要时重新读取它们。
可阻止在屏障前后的编译器优化,但硬件能完成自己的重新排序。
其实linux/kernel.h>中并没有这个函数,因为它是在kernel.h包含的头文件compiler.h中定义的*/
#includelinux/compiler.h>
#definebarrier()__memory_barrier()
#includeasm/system.h>
voidrmb(void);/*保证任何出现于屏障前的读在执行任何后续的读之前完成*/
voidwmb(void);/*保证任何出现于屏障前的写在执行任何后续的写之前完成*/
voidmb(void);/*保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成*/
voidread_barrier_depends(void);/*
一种特殊的、弱些的读屏障形式。
rmb阻止屏障前后的所有读指令的重新排序,read_barrier_depends
只阻止依赖于其他读指令返回的数据的读指令的重新排序。
区别微小,且不在所有体系中存在。
除非你确切地理解它们的差别,
并确信完整的读屏障会增加系统开销,否则应当始终使用rmb。
*/
/*以上指令是barrier的超集*/
voidsmp_rmb(void);
voidsmp_read_barrier_depends(void);
voidsmp_wmb(void);
voidsmp_mb(void);
/*仅当内核为SMP系统编译时插入硬件屏障;否则,它们都扩展为一个简单的屏障调用。
*/
典型的应用:
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);
内存屏障影响性能,所以应当只在确实需要它们的地方使用。
不同的类型对性能的影响也不同,因此要尽可能地使用需要的特定类型。
值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t,也可作为内存屏障使用。
某些体系允许赋值和内存屏障组合,以提高效率。
它们定义如下:
#defineset_mb(var,value)do{var=value;mb();}while0
/*以下宏定义在ARM体系中不存在*/
#defineset_wmb(var,value)do{var=value;wmb();}while0
#defineset_rmb(var,value)do{var=value;rmb();}while0
使用do...while结构来构造宏是标准C的惯用方法,它保证了扩展后的宏可在所有上下文环境中被作为一个正常的C语句执行。
使用I/O端口
I/O端口是驱动用来和许多设备之间的通讯方式。
I/O端口分配
在尚未取得端口的独占访问前,不应对端口进行操作。
内核提供了一个注册用的接口,允许驱动程序声明它需要的端口:
#includelinux/ioport.h>
structresource*request_region(unsignedlongfirst,unsignedlongn,constchar*name);/*告诉内核:
要使用从first开始的n个端口,name参数为设备名。
若分配成功返回非NULL,否则将无法使用需要的端口。
*/
/*所有的的端口分配显示在/proc/ioports中。
若不能分配到需要的端口,则可以到这里看看谁先用了。
*/
/*当用完I/O端口集(可能在模块卸载时),应当将它们返回给系统*/
voidrelease_region(unsignedlongstart,unsignedlongn);
intcheck_region(unsignedlongfirst,unsignedlongn);
/*检查一个给定的I/O端口集是否可用,若不可用,返回值是一个负错误码。
不推荐使用*/
操作I/O端口
在驱动程序注册I/O端口后,就可以读/写这些端口。
大部分硬件会把8、16和32位端口区分开,不能像访问系统内存那样混淆使用。
驱动必须调用不同的函数来存取不同大小的端口。
只支持内存映射的I/O寄存器的计算机体系通过重新映射I/O端口到内存地址来伪装端口I/O。
为了提高移植性,内核向驱动隐藏了这些细节。
Linux内核头文件(体系依赖的头文件)定义了下列内联函数(有的体系是宏,有的不存在)来访问I/O端口:
unsignedinb(unsignedport);
voidoutb(unsignedcharbyte,unsignedport);
/*读/写字节端口(8位宽)。
port参数某些平台定义为unsignedlong,有些为unsignedshort。
inb的返回类型也体系而不同。
*/
unsignedinw(unsignedport);
voidoutw(unsignedshortword,unsignedport);
/*访问16位端口(一个字宽)*/
unsignedinl(unsignedport);
voidoutl(unsignedlongword,unsignedport);
/*访问32位端口。
longword声明有的平台为unsignedlong,有的为unsignedint。
*/
在用户空间访问I/O端口
以上函数主要提供给设备驱动使用,但它们也可在用户空间使用,至少在PC上可以。
GNUC库在 中定义了它们。
如果在用户空间代码中使用必须满足以下条件:
(1)程序必须使用-O选项编译来强制扩展内联函数。
(2)必须用ioperm和iopl系统调用(#include)来获得对端口I/O操作的权限。
ioperm为获取单独端口操作权限,而iopl为整个I/O空间的操作权限。
(x86特有的)
(3)程序以root来调用ioperm和iopl,或是其父进程必须以root获得端口操作权限。
(x86特有的)
若平台没有ioperm和iopl系统调用,用户空间可以仍然通过使用/dev/prot设备文件访问I/O端口。
注意:
这个文件的定义是体系相关的,并且I/O端口必须先被注册。
串操作
除了一次传输一个数据的I/O操作,一些处理器实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作指
令。
它们完成任务比一个C语言循环更快。
下列宏定义实现了串I/O,它们有的通过单个机器指令实现;但如果目标处理器没有进行串I/O
的指令,则通过执行一个紧凑的循环实现。
有的体系的原型如下:
voidinsb(unsignedport,void*addr,unsignedlongcount);
voidoutsb(unsignedport,void*addr,unsignedlongcount);
voidinsw(unsignedport,void*addr,unsignedlongcount);
voidoutsw(unsignedport,void*addr,unsignedlongcount);
voidinsl(unsignedport,void*addr,unsignedlongcount);
voidoutsl(unsignedport,void*addr,unsignedlongcount);
使用时注意:
它们直接将字节流从端口中读取或写入。
当端口和主机系统有不同的字节序时,会导致不可预期的结果。
使用inw读取端口应在必要时自行转换字节序,以匹配主机字节序。
暂停式I/O
为了匹配低速外设的速度,有时若I/O指令后面还紧跟着另一个类似的I/O指令,就必须在I/O指令后面插入一个小延时。
在
这种情况下,可以使用暂停式的I/O函数代替通常的I/O函数,它们的名字以_p结尾,如inb_p、outb_p等等。
这些函数定义被大部分体系支持,尽管它们常常被扩展为与非暂停式I/O
同样的代码。
因为如果体系使用一个合理的现代外设总线,就没有必要额外暂停。
细节可参考平台的asm子目录的io.h
文件。
以下是include\asm-arm\io.h中的宏定义:
#defineoutb_p(val,port) outb((val),(port))
#defineoutw_p(val,port) outw((val),(port))
#defineoutl_p(val,port) outl((val),(port))
#defineinb_p(port) inb((port))
#defineinw_p(port) inw((port))
#defineinl_p(port) inl((port))
#defineoutsb_p(port,from,len) outsb(port,from,len)
#defineoutsw_p(port,from,len) outsw(port,from,len)
#defineoutsl_p(port,from,len) outsl(port,from,len)
#defineinsb_p(port,to,len) insb(port,to,len)
#defineinsw_p(port,to,len) insw(port,to,len)
#defineinsl_p(port,to,len) insl(port,to,len)
由此可见,由于ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O同样的代码。
平台相关性
由于自身的特性,I/O指令与处理器密切相关的,非常难以隐藏系统间的不同。
所以大部分的关于端口I/O的源码是平台依赖的。
以下是x86和ARM所使用函数的总结:
IA-32(x86)
x86_64
这个体系支持所有的以上描述的函数,端口号是unsignedshort类型。
ARM
端口映射到内存,支持所有函数。
串操作用C语言实现。
端口是unsignedint类型。
使用I/O内存
除了x86上普遍使用的I/O
端口外,和设备通讯另一种主要机制是通过使用映射到内存的寄存器或设备内存,统称为I/O内存。
因为寄存器和内存之间的区别对软件是透明的。
I/O
内存仅仅是类似RAM的一个区域,处理器通过总线访问这个区域,以实现设备的访问。
根据平台和总线的不同,I/O
内存可以就是否通过页表访问分类。
若通过页表访问,内核必须首先安排物理地址使其对设备驱动程序可见,在进行任何I/O之前必须调用
ioremap。
若不通过页表,I/O内存区域就类似I/O端口,可以使用适当形式的函数访问它们。
因为“sideeffect”的影响,不管是否需要ioremap,都不鼓励直接使用I/O内存的指针。
而使用专用的I/O内存操作函数,不仅在所有平台上是安全,而且对直接使用指针操作I/O内存的情况进行了优化。
I/O内存分配和映射
I/O内存区域使用前必须先分配,函数接口在定义:
structresource*request_mem_region(unsignedlongstart,unsignedlonglen,char*name);/*从start开始,分配一个len字节的内存区域。
成功返回一个非NULL指针,否则返回NULL。
所有的I/O内存分配情况都/proc/iomem中列出。
*/
/*I/O内存区域在不再需要时应当释放*/
voidrelease_mem_region(unsignedlongstart,unsignedlonglen);
/*一个旧的检查I/O内存区可用性的函数,不推荐使用*/
intcheck_mem_region(unsignedlongstart,unsignedlonglen);
然后必须设置一个映射,由ioremap函数实现,此函数专门用来为I/O
内存区域分配虚拟地址。
经过ioremap之后,设备驱动即可访问任意的I/O内存地址。
注意:
ioremap
返回的地址不应当直接引用;应使用内核提供的accessor函数。
以下为函数定义:
#includeasm/io.h>
void*ioremap(unsignedlongphys_addr,unsignedlongsize);
void*ioremap_nocache(unsignedlongphys_addr,unsignedlongsize);/*如果控制寄存器也在该区域,应使用的非缓存版本,以实现sideeffect。
*/
voidiounmap(void*addr);
访问I/O内存
访问I/O内存的正确方式是通过一系列专用于此目的的函数(在中定义的):
/*I/O内存读函数*/
unsignedintioread8(void*addr);
unsignedintioread16(void*addr);
unsignedintioread32(void*addr);
/*addr是从ioremap获得的地址(可能包含一个整型偏移量),返回值是从给定I/O内存读取的值*/
/*对应的I/O内存写函数*/
voidiowrite8(u8value,void*addr);
voidiowrite16(u16value,void*addr);
voidiowrite32(u32value,void*addr);
/*读和写一系列值到一个给定的I/O内存地址,从给定的buf读或写count个值到给定的addr*/
voidioread8_rep(void*addr,void*buf,unsignedlongcount);
voidioread16_rep(void*addr,void*buf,unsignedlongcount);
voidioread32_rep(void*addr,void*buf,unsignedlongcount);
voidiowrite8_rep(void*addr,constvoid*buf,unsignedlongcount);
voidiowrite16_rep(void*addr,constvoid*buf,unsignedlongcount);
voidiowrite32_rep(void*addr,constvoid*buf,unsignedlongcount);
/*需要操作一块I/O地址,使用一下函数*/
voidmemset_io(void*addr,u8value,unsignedintcount);
voidmemcpy_fromio(void*dest,void*source,unsignedintcount);
voidmemcpy_toio(void*dest,void*source,unsignedintcount);
/*旧函数接口,仍可工作,但不推荐。
*/
unsignedreadb(address);
unsignedreadw(address);
unsignedreadl(address);
voidwriteb(unsignedvalue,address);
voidwritew(unsignedvalue,address);
voidwritel(unsignedvalue,address);
像I/O内存一样使用端口
一些硬件有一个有趣的特性:
一些版本使用I/O端口,而其他的使用I/O内存。
为了统一编程接口,使驱动程序易于编写,2.6内核提供了一个ioport_map函数:
void*ioport_map(unsignedlongport,unsignedintcount);/*重映射count个I/O端口,使其看起来像I/O内存。
,此后,驱动程序可以在返回的地址上使用ioread8和同类函数。
其在编程时消除了I/O端口和I/O内存的区别。
/*这个映射应当在它不再被使用时撤销:
*/
voidioport_unmap(void*addr);
/*注意:
I/O端口仍然必须在重映射前使用request_region分配I/O端口。
ARM9不支持这两个函数!
*/
上面是基于《Linux设备驱动程序(第3版)》的介绍,以下分析ARM9的s3c2440A的linux驱动接口。
ARM9的linux驱动接口
s3c24x0处理器是使用I/O内存的,也就是说:
他们的外设接口是通过读写相应的寄存器实现的,这些寄存器和内存是使用单一的地址空间,并使用和读写内存一样的指令。
所以推荐使用I/O内存的相关指令。
但这并不表示I/O端口的指令在s3c24x0中不可用。
但是只要你注意其源码,你就会发现:
其实I/O端口的指令只是一个外壳,内部还是使用和I/O内存一样的代码。
以下列出一些:
I/O端口
#defineoutb(v,p) __raw_writeb(v,__io(p))
#defineoutw(v,p) __raw_writew((__force__u16)\