第 9 章 与硬件通讯
在硬件级别上, 内存区和 I/O 区域没有概念上的区别: 它们都是通过在地址总线和控制总线上发出电信号来存取(即, 读写信号)[32]并且读自或者写到数据总线. 也有例外,有些cpu在芯片上一个单个地址空间, 如:x86有人开的读和写电线给I/O端口和特殊的cpu指令存取端口,也有的在芯片组或cpu中附加额外的电路,后一种方法在很多嵌入式cpu中出现较多。
硬件缓冲的问题是最易面对的:底层的硬件已经配置(或者自动地或者通过 Linux 初始化代码)成禁止任何硬件缓冲, 当存取 I/O 区时(不管它们是内存还是端口区域).
对编译器优化和硬件重编排的解决方法是安放一个内存屏障。
#include <linux/kernel.h>
void barrier(void)
这个函数告知编译器插入一个内存屏障但是对硬件没有影响. 编译的代码将所有的当前改变的并且驻留在 CPU 寄存器的值存储到内存, 并且后来重新读取它们当需要时. 对屏障的调用阻止编译器跨越屏障的优化, 而留给硬件自由做它的重编排.
#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
屏障的这些版本仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用.
I/O 端口分配
#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);
还有一个函数以允许你的驱动来检查是否一个给定的 I/O 端口组可用:
int check_region(unsigned long first, unsigned long n);
但是一般不去使用这个函数,因为无法保证能获取端口,这是由于非原子性造成。
I/O 端口操作
Linux 内核头文件(特别地, 体系依赖的头文件 <asm/io.h>) 定义了下列内联函数来存取 I/O 端口:
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
读或写字节端口( 8 位宽 ). port 参数定义为 unsigned long 在某些平台以及 unsigned short 在其他的上. inb 的返回类型也是跨体系而不同的.
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
这些函数存取 16-位 端口( 一个字宽 ); 在为 S390 平台编译时它们不可用, 它只支持字节 I/O.
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
这些函数存取 32-位 端口. longword 声明为或者 unsigned long 或者 unsigned int, 根据平台. 如同字 I/O, "Long" I/O 在 S390 上不可用.
从用户空间访问I/O端口
下列条件应当应用来对于 inb 及其友在用户空间代码中使用:
· 程序必须使用 -O 选项编译来强制扩展内联函数.
· ioperm 和 iopl 系统调用必须用来获得权限来进行对端口的 I/O 操作. ioperm 为单独端口获取许可, 而 iopl 为整个 I/O 空间获取许可. 这 2 个函数都是 x86 特有的.
· 程序必须作为 root 来调用 ioperm 或者 iopl.[34] 可选地, 一个它的祖先必须已赢得作为 root 运行的端口权限.
如果没有 ioperm 和 iopl系统调用,则用户空间可以通过/dev/port设备文件访问I/O端口
可以尝试在程序中调用setuid root这样就不用显式获取特权了。
技术上来说,必须有CAP_SYS_RAWIO权限
字符串操作
直接的串操作比c下的循环效率要高很多,
字串函数的原型是:
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
读或写从内存地址 addr 开始的 count 字节. 数据读自或者写入单个 port 端口.
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
读或写 16-位 值到一个单个 16-位 端口.
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
读或写 32-位 值到一个单个 32-位 端口.
有件事要记住, 当使用字串函数时: 它们移动一个整齐的字节流到或自端口. 当端口和主系统有不同的字节对齐规则, 结果可能是令人惊讶的. 使用 inw 读取一个端口交换这些字节, 如果需要, 来使读取的值匹配主机字节序. 字串函数, 相反, 不进行这个交换.
暂停式I/O
一些平台 - 最有名的 i386 - 可能有问题当处理器试图太快传送数据到或自总线. 当处理器对于外设总线被过度锁定时可能引起问题( 想一下 ISA )并且可能当设备单板太慢时表现出来. 解决方法是插入一个小的延时在每个 I/O 指令后面, 如果跟随着另一个指令. 在 x86 上, 这个暂停是通过进行一个 outb 指令到端口 0x80 ( 正常地不是常常用到 )实现的, 或者通过忙等待. 细节见你的平台的 asm 子目录的 io.h 文件.
平台依赖性
IA-32 (x86)
x86_64
这个体系支持所有的本章描述的函数. 端口号是 unsigned short 类型.
IA-64 (Itanium)
支持所有函数; 端口是 unsigned long(以及内存映射的)). 字串函数用 C 实现.
Alpha
支持所有函数, 并且端口是内存映射的. 端口 I/O 的实现在不同 Alpha 平台上是不同的, 根据它们使用的芯片组. 字串函数用 C 实现并且定义在 arch/alpha/lib/io.c 中定义. 端口是 unsigned long.
ARM
端口是内存映射的, 并且支持所有函数; 字串函数用 C 实现. 端口是 unsigned int 类型.
Cris
这个体系不支持 I/O 端口抽象, 甚至在一个模拟模式; 各种端口操作定义成什么不做.
M68k
M68k
端口是内存映射的. 支持字串函数, 并且端口类型是 unsigned char.
MIPS
MIPS64
MIPS 端口支持所有的函数. 字串操作使用紧凑汇编循环来实现, 因为处理器缺乏机器级别的字串 I/O. 端口是内存映射的; 它们是 unsigned long.
PA
支持所有函数; 端口是 int 在基于 PCI 的系统上以及 unsigned short 在 EISA 系统, 除了字串操作, 它们使用 unsigned long 端口号.
PowerPC
PowerPC64
支持所有函数; 端口有 unsigned char * 类型在 32-位 系统上并且 unsigned long 在 64-位 系统上.
S390
类似于 M68k, 这个平台的头文件只支持字节宽的端口 I/O, 而没有字串操作. 端口是 char 指针并且是内存映射的.
Super
端口是 unsigned int ( 内存映射的 ), 并且支持所有函数.
SPARC SPARC64
再一次, I/O 空间是内存映射的. 端口函数的版本定义来使用 unsigned long 端口.
好奇的读者能够从 io.h 文件中获得更多信息, 这个文件有时定义几个结构特定的函数, 加上我们在本章中描述的那些. 但是, 警告有些这些文件是相当难读的.
使用I/O内存
I/O 端口在 x86 世界中流行, 用来和设备通讯的主要机制是通过内存映射的寄存器和设备内存. 2 者都称为 I/O 内存, 因为寄存器和内存之间的区别对软件是透明的.
I/O内存访问的方法和计算机体系架构有关,不过原理相同,I/O 内存可以或者不可以通过页表来存取. 当通过页表存取, 内核必须首先安排从你的驱动可见的物理地址, 并且这常常意味着你必须调用 ioremap 在做任何 I/O 之前. 如果不需要页表, 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);
这个函数分配一个 len 字节的内存区, 从 start 开始. 如果一切顺利, 一个非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 内存读, 使用下列之一:
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
这里, addr 应当是从 ioremap 获得的地址(也许与一个整型偏移); 返回值是从给定 I/O 内存读取的.
有类似的一系列函数来写 I/O 内存:
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
如果你必须读和写一系列值到一个给定的 I/O 内存地址, 你可以使用这些函数的重复版本:
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr, const void *buf, unsigned long count);
这些函数读或写 count 值从给定的 buf 到 给定的 addr. 注意 count 表达为在被写入的数据大小; ioread32_rep 读取 count 32-位值从 buf 开始.
上面描述的函数进行所有的 I/O 到给定的 addr. 如果, 相反, 你需要操作一块 I/O 地址, 你可使用下列之一:
void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);
如果你通览内核源码, 你可看到许多调用旧的一套函数, 当使用 I/O 内存时. 这些函数仍然可以工作, 但是它们在新代码中的使用不鼓励. 除了别的外, 它们较少安全因为它们不进行同样的类型检查. 但是, 我们在这里描述它们:
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
这些宏定义用来从 I/O 内存获取 8-位, 16-位, 和 32-位数据值.
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
如同前面的函数, 这些函数(宏)用来写 8-位, 16-位, 和 32-位数据项.
一些 64-位平台也提供 readq 和 writeq, 为 PCI 总线上的 4-字(8-字节)内存操作. 这个 4-字 的命名是一个从所有的真实处理器有 16-位 字的时候的历史遗留. 实际上, 用作 32-位 值的 L 命名也已变得不正确, 但是命名任何东西可能使事情更混淆.