`

【Linux 驱动】第九章 与硬件通信

 
阅读更多

在学习有关I/O总线的内容时,最好先看看相关的知识:从PC总线到ARM的内部总线

一,I/O 端口和 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(Industrial Standard Architecture,工业标准结构总线)设备普遍使用 I/O 端口,但大部分 PCI 设备则把寄存器映射到某个内存地址区,这种 I/O 内存方法通常是首选的。因为它无需使用特殊的处理器指令,CPU 核访问内存更有效率,且编译器在访问内存时在寄存器分配和寻址模式的选择上有更多自由。

二,I/O 寄存器和常规内存
在进入这部分学习的时候,首先要理解一个概念:side effect,书中译为边际效应,第二版译为副作用。我觉得不管它是怎么被翻译的,都不可能精准表达原作者的意思,所以我个人认为记住side effect就好。下面来讲讲side effect的含义。我先贴出两个网上已有的两种说法(在这里谢谢两位高人的分享):
第一种说法:
side effect(译为边际效应或副作用):是指读取某个地址时可能导致该地址内容发生变化,比如,有些设备的中断状态寄存器只要一读取,便自动清零。I/O寄存器的操作具有side effect,因此,不能对其操作不能使用cpu缓存。
原文网址:http://qinbh.blog.sohu.com/62733495.html
第二种说法:
说一下我的理解:I/O端口与实际外部设备相关联,通过访问I/O端口控制外部设备,“边际效应”是指控制设备(读取或写入)生效,访问I/O口的主要目的就是边际效应,不像访问普通的内存,只是在一个位置存储或读取一个数值,没有别的含义了。我是基于ARM平台理解的,在《linux设备驱动程序》第二版中的说法是“副作用”,不是“边际效应”。
原文网址:http://linux.chinaunix.net/bbs/viewthread.php?tid=890636&page=1#pid6312646


结合以上两种说法和自己看《Linux设备驱动程序(第3版)》的理解,我个人认为可以这样解释:


side effect 是指:访问I/O寄存器时,不仅仅会像访问普通内存一样影响存储单元的值,更重要的是它可能改变CPU的I/O端口电平输出时序CPU对I/O端口电平的反应等等,从而实现CPU的控制功能。CPU在电路中的意义就是实现其side effect 。

I/O 寄存器和 RAM 的主要不同就是 I/O 寄存器操作有side effect, 而内存操作没有。因为存储单元的访问速度对 CPU 性能至关重要,编译器会对源代码进行优化,主要是: 使用高速缓存保存数值 和 重新编排读/写指令顺序。但对I/O 寄存器操作来说,这些优化可能造成致命错误。因此,驱动程序必须确保在操作I/O 寄存器时,不使用高速缓存,且不能重新编排读/写指令顺序
解决方法:
硬件缓存问题:只要把底层硬件配置(自动地或者通过 Linux 初始化代码)成当访问I/O 区域时(不管内存还是端口)禁止硬件缓存即可。 硬件指令重新排序问题:在硬件(或其他处理器)必须以一个特定顺序执行的操作之间设置内存屏障(memory barrier)。
Linux 提供以下宏来解决所有可能的排序问题:

一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小, 且不在所有体系中存在。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。

/*以上指令是barrier的超集*/

/*仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用。*/


设备驱动中使用内存屏障的典型应用:


使用do...while 结构来构造宏是标准 C 的惯用方法,它保证了扩展后的宏可在所有上下文环境中被作为一个正常的 C 语句执行。

三,使用 I/O 端口
I/O 端口是驱动用来和许多设备之间的通讯方式。
1)I/O 端口分配

在尚未取得端口的独占访问前,不应对端口进行操作。内核提供了一个注册用的接口,允许驱动程序声明它需要的端口:
#include <linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n, const char *name);//告诉内核:要使用从 first 开始的 n 个端口,name 参数为设备名。若分配成功返回非 NULL,否则将无法使用需要的端口。


所有的的端口分配显示在 /proc/ioports 中。若不能分配到需要的端口,则可以到这里看看谁先用了。


void release_region(unsigned long start, unsigned long n);//当用完 I/O 端口集(可能在模块卸载时), 应当将它们返回给系统

int check_region(unsigned long first, unsigned long n); //检查一个给定的 I/O 端口集是否可用,若不可用, 返回值是一个负错误码。不推荐使用


2)操作 I/O 端口

在驱动程序注册I/O 端口后,就可以读/写这些端口。大部分硬件会把8、16和32位端口区分开,不能像访问系统内存那样混淆使用。驱动必须调用不同的函数来存取不同大小的端口。
只支持内存映射的 I/O 寄存器的计算机体系通过重新映射I/O端口到内存地址来伪装端口I/O。为了提高移植性,内核向驱动隐藏了这些细节。Linux 内核头文件(体系依赖的头文件 <asm/io.h> ) 定义了下列内联函数(有的体系是宏,有的不存在)来访问 I/O 端口:


3)在用户空间访问 I/O 端口

以上函数主要提供给设备驱动使用,但它们也可在用户空间使用,至少在 PC上可以。 GNU C 库在 <sys/io.h> 中定义了它们。如果在用户空间代码中使用必须满足以下条件:

(1)程序必须使用 -O 选项编译来强制扩展内联函数。
(2)必须用ioperm 和 iopl 系统调用(#include <sys/perm.h>) 来获得对端口 I/O 操作的权限。ioperm 为获取单独端口操作权限,而 iopl 为整个 I/O 空间的操作权限。 (x86 特有的)
(3)程序以 root 来调用 ioperm和 iopl,或是其父进程必须以 root 获得端口操作权限。(x86 特有的)
若平台没有 ioperm 和 iopl 系统调用,用户空间可以仍然通过使用 /dev/prot 设备文件访问 I/O 端口。注意:这个文件的定义是体系相关的,并且I/O 端口必须先被注册。


4)串操作

除了一次传输一个数据的I/O操作,一些处理器实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作指令。它们完成任务比一个 C 语言循环更快。下列宏定义实现了串I/O,它们有的通过单个机器指令实现;但如果目标处理器没有进行串 I/O 的指令,则通过执行一个紧凑的循环实现。 有的体系的原型如下:


使用时注意: 它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。 使用 inw 读取端口应在必要时自行转换字节序,以匹配主机字节序。


5)暂停式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中的宏定义:


由此可见,由于ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。


6)平台相关性

由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:
IA-32 (x86)
x86_64 :这个体系支持所有的以上描述的函数,端口号是 unsigned short 类型。
ARM :端口映射到内存,支持所有函数。串操作 用C语言实现。端口是 unsigned int 类型。

四,使用 I/O 内存
除了x86上普遍使用的I/O 端口外,和设备通讯另一种主要机制是通过使用映射到内存的寄存器设备内存,统称为 I/O 内存。因为寄存器和内存之间的区别对软件是透明的。I/O 内存仅仅是类似 RAM 的一个区域,处理器通过总线访问这个区域,以实现设备的访问。
根据平台和总线的不同,I/O 内存可以就是否通过页表访问分类。若通过页表访问,内核必须首先安排物理地址使其对设备驱动程序可见,在进行任何 I/O 之前必须调用 ioremap。若不通过页表,I/O 内存区域就类似I/O 端口,可以使用适当形式的函数访问它们。因为“side effect”的影响,不管是否需要 ioremap,都不鼓励直接使用 I/O 内存的指针。而使用专用的 I/O 内存操作函数,不仅在所有平台上是安全,而且对直接使用指针操作 I/O 内存的情况进行了优化。


五,I/O 内存分配和映射

I/O 内存区域使用前必须先分配,函数接口在 <linux/ioport.h> 定义:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);/* 从 start 开始,分配一个 len 字节的内存区域。成功返回一个非NULL指针,否则返回NULL。所有的 I/O 内存分配情况都 /proc/iomem 中列出。*/


void release_mem_region(unsigned long start, unsigned long len); //I/O内存区域在不再需要时应当释放


int check_mem_region(unsigned long start, unsigned long len);//一个旧的检查 I/O 内存区可用性的函数,不推荐使用


然后必须设置一个映射,由 ioremap 函数实现,此函数专门用来为I/O 内存区域分配虚拟地址。经过ioremap之后,设备驱动即可访问任意的 I/O 内存地址。注意:ioremap 返回的地址不应当直接引用;应使用内核提供的 accessor 函数。以下为函数定义:

#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);/*如果控制寄存器也在该区域,应使用的非缓存版本,以实现side effect。*/
void iounmap(void * addr);


六,访问I/O 内存

访问I/O 内存的正确方式是通过一系列专用于此目的的函数(在 <asm/io.h> 中定义的):

七,像I/O 内存一样使用端口

一些硬件有一个有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 内存。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:
void *ioport_map(unsigned long port, unsigned int count);/*重映射 count 个I/O 端口,使其看起来像 I/O 内存。,此后,驱动程序可以在返回的地址上使用 ioread8 和同类函数。其在编程时消除了I/O 端口和I/O 内存的区别。


void ioport_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端口
#define outb(v,p)__raw_writeb(v,__io(p))
#define outw(v,p)__raw_writew((__force __u16) \
cpu_to_le16(v),__io(p))
#define outl(v,p)__raw_writel((__force __u32) \
cpu_to_le32(v),__io(p))

#define inb(p)({ __u8 __v = __raw_readb(__io(p)); __v; })
#define inw(p)({ __u16 __v = le16_to_cpu((__force __le16) \
__raw_readw(__io(p))); __v; })
#define inl(p)({ __u32 __v = le32_to_cpu((__force __le32) \
__raw_readl(__io(p))); __v; })
I/O内存
#define ioread8(p)({ unsigned int __v = __raw_readb(p); __v; })
#define ioread16(p)({ unsigned int __v = le16_to_cpu(__raw_readw(p)); __v; })
#define ioread32(p)({ unsigned int __v = le32_to_cpu(__raw_readl(p)); __v; })

#define iowrite8(v,p)__raw_writeb(v, p)
#define iowrite16(v,p)__raw_writew(cpu_to_le16(v), p)
#define iowrite32(v,p)__raw_writel(cpu_to_le32(v), p)
对I/O端口的指令和I/O内存的指令写相应的驱动程序,在这里值得注意的有4点:
(1)所有的读写指令所赋的地址必须都是虚拟地址,你有两种选择:使用内核已经定义好的地址,如 S3C2440_GPJCON等等,这些都是内核定义好的虚拟地址,有兴趣的可以看源码。还有一种方法就是使用自己用ioremap映射的虚拟地址。绝对不能使用实际的物理地址,否则会因为内核无法处理地址而出现oops。
(2)在使用I/O指令时,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因为request的功能只是告诉内核端口被谁占用了,如再次request,内核会制止。
(3)在使用I/O指令时,所赋的地址数据有时必须通过强制类型转换为 unsigned long ,不然会有警告(具体原因请看Linux设备驱动程序学习(7)-内核的数据类型) 。虽然你的程序可能也可以使用,但是最好还是不要有警告为妙。
(4)在include\asm-arm\arch-s3c2410\hardware.h中定义了很多io口的操作函数,有需要可以在驱动中直接使用,很方便

分享到:
评论

相关推荐

    Linux驱动程序开发第三版

    《Linux驱动程序开发第三版》是一本专注于Linux操作系统下驱动程序开发的专业书籍,适用于那些希望深入理解Linux内核机制和想要提升驱动程序编写能力的开发者。本书详细阐述了如何在Linux环境中设计、实现和调试设备...

    Linux设备驱动程序.pdf

    第9章讲述了与硬件通讯的方法。硬件设备通常通过I/O端口或I/O内存进行数据交换,本章详细介绍了I/O端口和I/O内存的使用,以及如何通过示例来说明这些概念。 第10章专注于中断处理。中断是硬件设备通知CPU需要处理...

    Linux驱动程序开发实例2版源码.zip

    通过这些源码,学习者可以逐步了解Linux驱动程序的各个组成部分,如何与硬件通信,以及如何利用内核提供的接口进行设备管理。在实践中运行这些代码,有助于加深对Linux内核机制的理解,提升驱动开发能力。

    Linux驱动程序设计第二版_pdf

    Linux驱动程序设计是Linux系统开发中的核心部分,它连接了硬件设备与操作系统,使得用户空间的应用程序能够通过标准接口与硬件进行通信。驱动程序的主要任务包括初始化硬件、处理硬件中断、数据传输以及对硬件状态的...

    linux驱动开发 LDD3

    《Linux设备驱动程序第三版》是学习Linux驱动开发的重要教材,书中包含丰富的实例和详细解释,帮助读者深入理解Linux驱动开发的各个方面。通过阅读和实践书中的内容,开发者可以掌握编写高效、可靠的Linux驱动程序的...

    Linux驱动程序开发第三版_pdf(linux device drivers)

    《Linux驱动程序开发第三版》是Linux世界中一本经典的设备驱动程序开发指南,它深入浅出地介绍了如何为Linux操作系统编写和理解设备驱动程序。这本书不仅涵盖了基础理论,还提供了丰富的实践案例,使得读者能够更好...

    Linux 设备驱动开发 全23章

    Linux设备驱动开发是一个复杂而关键的领域,它涉及到操作系统如何与硬件进行通信,以实现高效、可靠的系统运行。本资源包含全23章的详细内容,涵盖了Linux设备驱动的各个方面,对于想要深入学习这一领域的开发者来说...

    驱动linux课件驱动linux课件

    Linux驱动程序是操作系统与硬件设备之间的重要桥梁,它允许Linux系统识别并有效利用硬件资源。在深入探讨Linux驱动开发之前,我们先理解一下驱动程序的基本概念。驱动程序是一种特殊的软件,它提供了操作系统与硬件...

    LINUX设备驱动程序第三版源码

    9. **LINUX驱动sculld**:可能是一个与"scull"相关的守护进程(daemon),用于管理"scull"设备的运行时操作,如动态调整设备参数或监控设备状态。 通过学习和分析这些源码,读者可以了解Linux设备驱动的基本结构、...

    Linux驱动实例

    7. **硬件接口编程**:针对不同的硬件设备,学习如何与硬件进行通信,包括GPIO、I2C、SPI等接口的编程。 8. **调试技巧**:介绍如何使用kernel log、dmesg、gdb等工具进行驱动程序的调试。 9. **实例分析**:书中...

    linux设备驱动程序第二版

    这本书详细阐述了Linux内核与硬件交互的基本原理,是学习Linux驱动开发不可或缺的参考资料。下面将深入探讨书中涉及的主要知识点。 一、Linux内核基础 在讲解驱动程序之前,书中首先介绍了Linux内核的基础知识,...

    Linux驱动程序开发第三版-英文

    《Linux驱动程序开发第三版》是一本深入探讨Linux操作系统下驱动程序开发的专业书籍。这本书针对的是有经验的程序员,特别是那些对操作系统内核机制有一定了解的开发者。在Linux系统中,驱动程序是连接硬件设备与...

    LINUX设备驱动第三版_588及代码.rar

    第九章 与硬件通信 I/O端口和I/O内存 使用I/O端口 I/O端口示例 使用I/O内存 快速参考 第十章 中断处理 准备并口 安装中断处理例程 实现中断处理例程 顶半部和底半部 中断共享 中断驱动的I/O 快速参考 ...

    《Linux 设备驱动开发详解》(宋宝华) 学习笔记.zip

    6. **中断处理**:中断是硬件与CPU通信的主要方式,学习如何编写中断处理程序,理解和使用软中断、底半部(bottom halves)和工作队列是驱动开发的关键。 7. **I/O端口和内存映射**:对于某些硬件,如PCI设备,你...

    LINUX 设备驱动开发详解 源码

    "LINUX设备驱动开发详解 源码"这套资源很可能是针对Linux内核设备驱动程序的一份详细教程,包含了源代码分析,对于学习和实践Linux驱动开发的人来说极具价值。 首先,我们要理解设备驱动是什么。设备驱动是操作系统...

    linux摄像头驱动实例

    - 驱动核心:实现与硬件交互的低级函数,如初始化、配置和关闭设备。 - 中断处理:处理来自摄像头的中断事件,例如帧完成通知。 - DMA传输:利用DMA控制器高效地从摄像头到内存传输数据。 - I/O控制:响应用户...

    linux 驱动开发详解

    - **驱动程序的角色**:Linux驱动程序是操作系统与硬件设备之间的桥梁,负责处理硬件操作并提供标准接口给上层软件。 - **驱动分类**:包括字符驱动、块设备驱动、网络驱动等,每种类型对应不同的设备和服务方式。...

    linux驱动开发.zip

    - Linux驱动程序的作用:驱动程序是操作系统与硬件之间的桥梁,它解释并执行来自操作系统或应用程序的命令,控制硬件设备。 - 驱动分类:字符设备驱动、块设备驱动、网络设备驱动等。 - 驱动开发流程:了解硬件、...

    linux设备驱动程序3(英文版完整)

    《Linux设备驱动程序3》是Linux内核编程领域的一本权威著作,主要涵盖了与设备驱动相关的各种技术。这本书的英文版提供了更为准确的信息,避免了中文翻译可能存在的误解。以下是对压缩包中各个章节主要内容的详细...

    6410-linux驱动程序的位置

    ### 6410-Linux驱动程序的位置及详细介绍 #### 一、概述 本文档旨在为Mini6410和Tiny6410开发板的用户介绍Linux系统中各种驱动程序的具体位置及其所对应的设备名。这两款开发板在硬件资源分配上保持了一致性,因此...

Global site tag (gtag.js) - Google Analytics