Linux设备驱动程序学习笔记——第九章 与硬件通信

Linux设备驱动程序学习笔记

第九章 与硬件通信

一、I/O端口和I/O内存
每种外设都通过读写寄存器进行控制。

(1)I/O寄存器和常规内存
注意避免由于CPU或编译器不恰当的优化而改变预期的I/O动作。
有硬件缓存引起的问题很好解决,只要把底层硬件配置成在访问I/O区域时禁止硬件缓存即可。
由编译器优化或硬件重新排序引起的问题解决办法,对硬件必须以特定顺序执行的操作之间设置内存屏障(memory barrier)。
Linux提供了4个宏:

#include<linux/kernel.h>
void barrier(void);
对barrier的调用可防止在屏障前后的编译器优化,但硬件能完成自己的重新排序。

#include<asm/system.h>
void rmb(void); /*保证任何出现于屏障前的读在执行任何后续的读之前完成*/
void wmb(void); /*保证任何出现于屏障前的写在执行任何后续的写之前完成*/
void mb(void); /*保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成*/
void read_barrier_depends(void); /*一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小, 且不在所有体系中存在。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_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);

二、使用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,失败,返回NULL

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位 端口( 一个字宽 )*/

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

 
/*访问 32位 端口。 longword 声明有的平台为 unsigned long ,有的为 unsigned int。*/
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端口
以上函数主要用在驱动程序的使用上的,但他们也可以在用户空间使用,至少在PC类计算机上可以使用。GNU的C库在<sys/io.h>中定义了这些函数。如果要在用户空间代码中使用inb及相关函数,则必须满足下面这些条件:

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

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

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

字节端口读写

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

16位端口读写

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

32位端口读写,即使在64位的体系结构上,端口地址也只使用最大32位的数据通路

使用 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 中列出。/

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

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

然后必须设置一个映射,由 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 内存读函数/
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 内存地址,从给定的 buf 读或写 count 个值到给定的 addr */
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);

/需要操作一块 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);

/旧函数接口,仍可工作, 但不推荐。/
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);

像 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不支持这两个函数!/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值