驱动开发系列03-如何与硬件通信

目录

一:概述

二:I/O端口和I/O内存的概念

三:硬件寄存器(I/O寄存器)和内存

四:使用I/O端口        

1. 分配I/O端口

2. 在驱动中使用I/O端口

3.在用户空间中使用I/O端口

4. I/O串操作(一次读写多个字节)

5. 暂停I/O读写 (停顿等待)

6. I/O操作对平台的依赖

7. 通用 I/O 端口概述

8. 并口概述

9. 一个简单的驱动例子 

五:使用I/O内存

1. I/O内存的分配和映射

2. I/O内存访问

3. I/O端口映射为I/O内存 (ioport_map)


一:概述

       驱动程序是软件与硬件之间的抽象层;因此,它需要与这两者对话,本文将向你展示驱动程序如何与硬件对话。并介绍I/O端口和I/O内存的概念。

二:I/O端口和I/O内存的概念

       CPU通过写入和读取硬件寄存器的方式来控制硬件设备。在大多数情况下,一个硬件设备有多个寄存器,可以通过连续的地址访问它们,我们将这些可被CPU读写的硬件寄存器称为I/O端口。在硬件层面,访问I/O端口是通过地址总线和控制总线发送读写控制信号,以及通过数据总线读取或写入数据来访问的。

       一些CPU制造商在其芯片内部采用统一的地址空间,认为访问设备寄存器和访问内存的方式相同,使用同一地址空间即可。而另一些则认为硬件设备不同于内存,因此应该有单独的地址空间,比如像最著名的x86系列就为I/O地址设置了独立的读写电气线路,并为访问改地址设置了特殊的CPU指令。出于上述原因,Linux在其运行的所有平台上都实现了I/O端口的概念,即使在使用统一地址空间的平台上也是如此。

        然而上面说的I/O端口并不是设备访问的唯一方式,目前大多数PCI设备都将硬件寄存器映射到内存地址区域,我们将这样的内存区域称为I/O内存,即可以像访问普通内存一样来访问设备,因此它不需要使用特殊的处理器指令,CPU访问内存的效率更高,而且编译器在访问内存时,在寄存器分配和寻址方式选择方面有更大的自由度。

三:硬件寄存器(I/O寄存器)和内存

        尽管硬件寄存器和内存之间有很大的相似性,但程序员访问I/O寄存器时必须小心谨慎,避免被CPU或编译器的优化所欺骗,因为这些优化会改变预期的I/O行为。

       I/O寄存器和内存的主要区别在于,CPU或编译器的优化会对I/O操作产生副作用,比如正常的内存写入操作是将一个值存储到一个位置,而内存读取则返回最后写入的值。但是由于内存访问速度对CPU性能至关重要,所以CPU或编译器通过多种方式对内存访问进行了优化,比如值被缓存,读/写指令被重新排序等。

      然而这些优化往往对I/O操作是有害的。处理器无法预料其他进程对I/O寄存器访问顺序的依赖情况,编译器或CPU可能会耍小聪明,重新安排你所请求的操作顺序;结果可能会出现难以调试的奇怪错误,因此驱动程序确保在访问寄存器时不执行缓存,也不进行读写重新排序。

       硬件缓存的问题最容易解决:底层硬件已经被配置(自动配置或由Linux初始化代码配置),以便在访问I/O区域(无论时内存还是端口区域)时禁用任何硬件缓存。

      编译器优化和读写指令重排的解决方案是,在必要的地方设置内存屏障,以使得硬件或处理器保证以特定的顺序得到之前操作的结果。Linux提供了四个函数来保证所有可能的排序需求:

#include <linux/kernel.h>
void barrier(void)

      上面这个barrier函数告诉编译器插入内存屏障,防止编译器跨屏障进行优化,在屏障内读写操作可以自由地进行重新排序,对硬件没有影响。编译后的代码会将所有当前已修改并驻留在CPU寄存器中的值存储到内存中,并在以后需要时重新读取。

#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);

       上面这些函数在编译指令流中插入硬件内存屏障;它们的实现取决于具体的平台(x86, arm,mips...), rmb(读内存屏障)确保在执行任何后续读操作之前,在屏障之前出现的任何读操作都均已完成。wmb(写内存屏障)是保证在执行任何后续写操作之前,在屏障之前出现的任何写操作均已完成。mb是兼具上面两者的功能。read_barrier_depends 是一种特殊的,较弱的读屏障形式,不常用。

void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);

       上面这些函数是当内核为SMP(对称多处理器)系统编译时使用的,当不是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);

       如前所述,必须确保所有对设备寄存器的操作都已按正确设置之后,然后才能开始设备寄存器做新的操作。

       内存屏障会强制要求安装必要的顺序完成写入操作。由于内存屏障会影响性能,因此只应在真正需要时使用。不同类型的内存屏障也有不同的性能特点,因此尽可能使用恰当的类型,例如,在x86平台上,wmb()目前不起任何作用,因此处理器外的写入不会重新排序,不过,读取时重新排序的,因此mb()比wmb()慢。

       值得注意的是,处理同步问题的大多数其他内核原语,如spinlock和atomic_t操作,也具有内存屏障的功能。同样值得注意的是,一些外设总线(如PCI总线)本身也有缓存问题,我们将在后面章节讨论这些问题。有些架构允许将赋值和内存屏障有效结合起来使用。内核提供了一些宏来使用这种组合,让驱动开发者使用更方便;它们的定义如下:

#define set_mb(var, value) do {var = value; mb( );} while 0
#define set_wmb(var, value) do {var = value; wmb( );} while 0
#define set_rmb(var, value) do {var = value; rmb( );} while 0

四:使用I/O端口        

      I/O端口是驱动程序与设备通信的手段,至少在过去是如此。本节将介绍使用I/O端口的各种函数;并且还将讨论一些可移植性问题。

1. 分配I/O端口

      一般来说在访问端口之前,一定要确保对端口拥有独占访问权,否则不应该访问 I/O 端口。内核提供了一个函数,允许驱动程序申请端口。该函数接口是 request_region:

#include <linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n,
const char *name)

      该函数的含义是,向内核申请 n 个端口,从first开始。name 为设备名称。如果分配成功,返回值为非 NULL。如果 request_region 返回 NULL,则无法使用所需的端口。

      所有已分配的端口都显示在 /proc/ioports。如果无法分配到所需的端口,就可以到这里看看是谁先在占用着这些端口。

     当I/O端口在使用完后,或在模块卸载时,应该将它们归还给系统:

void release_region(unsigned long start, unsigned long n);
2. 在驱动中使用I/O端口

     当成功申请了I/O端口后,就须对这些端口进行读取或写入了。大多数硬件会区分 8 位、16 位和 32 位端口。因此,C 程序必须调用不同的函数来访问不同大小的端口,在只支持内存映射 I/O 的计算机体系结构通过将端口地址重映射到内存地址来伪造端口 I/O,内核会向驱动程序隐藏这些细节。Linux内核提供了以下函数来访问 I/O 端口:

unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);

      上面这组函数用来访问8位端口(1个字节)。参数 port 在某些平台上被定义为unsigned long,而在其他平台上则被定义为unsigned short。inb 的返回类型在不同架构下也有所不同。

unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);

       上面这组函数用来访问16位端口(2个字节);在某些嵌入式平台上,这些函数不可用,因为该平台只支持字节 I/O。

unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);

       上面这组函数用来访问32位端口(4个字节);在某些嵌入式平台上,这些函数不可用,因为只支持字节I/O。

3.在用户空间中使用I/O端口

      刚才介绍的函数主要供设备驱动程序使用,但也可以在用户空间使用,至少在 PC 级计算机上是如此。GNU C 库在 <sys/io.h> 中定义了这些函数。要在用户空间代码中使用这些函数,必须满足以下条件:

    (1)编译程序时必须使用 -O 选项,以强制扩展内联函数。
    (2)必须使用 ioperm 或 iopl 系统调用来获取I/O端口执行权限。ioperm 获取单个端口的权限,而 iopl 则获取整个 I/O 空间的权限。这两个函数都是 x86 专用的。
   (3)程序必须以 root 运行才能调用 ioperm 或 iopl。或程序以 root 方式获得端口访问权限。

      如果不是x86平台,则没有 ioperm 和 iopl 系统调用,但用户空间仍可使用 /dev/port 设备文件访问 I/O 端口。但要注意的是,该文件的含义也与特定平台密切相关,/dev/prot也仅限于在PC上使用。

 4. I/O串操作(一次读写多个字节)

       除了在端口上输入和输出字节/2字节/4字节之外,一些处理器还实现了特殊指令,用于在端口端传输多个字节序列。这些指令就是所谓的字符串指令,它们比 C 语言循环更快地完成任务。这些函数实现了字符串 I/O 的概念,它们或者使用一条机器指令,或者在目标处理器没有执行字符串 I/O 的指令时执行一个紧凑的循环。在一些嵌入式平台上,没有定义这些宏。所以这些宏不是跨平台的,不具有可移植。只能在某些平台上使用,比如PC上。以下是这些函数的定义

void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);

从内存地址 addr 开始读取 count 字节数据并将其写入到端口 port 中。从端口读出 count 字节数据并将其写入到内存地址 addr 中。

void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);

从内存地址 addr 上开始读取 count 个字(16bit)数据并将其写入到端口 port 中。从端口读出 count个2字节(16bit)数据并将其写入到内存地址 addr 中。

void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);

从内存地址 addr 上开始读取 count 个4字节(32bit)数据并将其写入到端口 port 中。从端口读出 count 个4字节(32bit)数据并将其写入到内

  • 36
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑不溜秋的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值