Communicating with Hardware [LDD3 09]

本文深入探讨了I/O操作的原理,包括I/O端口和I/O内存的使用,详细解析了硬件交互过程中的关键概念和技术细节。文章涵盖了设备驱动程序如何控制硬件,I/O端口与内存映射寄存器的区别,以及如何正确访问和管理I/O资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Table of Contents

9.1. I/O Ports and I/O Memory

9.1.1. I/O Registers and Conventional Memory

9.2. Using I/O Ports

9.2.1. I/O Port Allocation

9.2.2. Manipulating I/O ports

9.2.3. I/O Port Access from User Space

9.4. Using I/O Memory

9.4.1. I/O Memory Allocation and Mapping

9.4.2. Accessing I/O Memory

9.4.3. Ports as I/O Memory

9.4.4. Reusing short for I/O Memory

9.4.5. ISA Memory Below 1 MB

9.4.6. isa_readb and Friends


本章开始讲一些跟hardware有关的操作,比如I/O port,register等。

device driver的角色要清楚,它只是中间层,向上有kernel,user application,向下就是actual hardware。作为driver,核心功能必然是驱动硬件。

9.1. I/O Ports and I/O Memory


每一个外设都要通过register来控制,大多数设备都有不止一个register,并且访问这些register的地址是连续,要么是IO地址空间,要么是内存地址空间。在实际haredware的角度,这二者没有本质区别,都是向地址总线和控制总线发送控制信号,并通过数据总线来读取。不过依赖于架构的实现,比如有些机构上device register和普通内存是统一的地址空间,有些是分开的。比如X86上,就有单独的总线来访问I/O port,并且有特殊的CPU指令访问这些ports。

因为所有的外设都要适配一定的总线,而这些总线已经集成在了PC中,为了支持IO port,某些没有单独IO port地址空间的CPU,甚至需要做一个fake的IO port读写。这种fake的IO读写,可以通过芯片组,也可以通过在CPU core中增加额外的模块,后者一般用在嵌入式系统中。

基于同样的原因,kernel为所有的platform都增加IO port概念的支持,即便某些架构上CPU支持统一寻址。而port的访问,有些情况下依赖于PC自己的实现。

尽管外设总线支持IO port,但是不是所有的设备都会把register map到IO port上。ISA外设通常会这么做,但是大部分PCI设备会把register map到内存地址空间。推荐使用内存地址空间,因为不需要特殊的CPU指令,而且CPU访问memory更高效,并且对编译器也更友好,因为只是访存指令,可以有针对性的优化。

9.1.1. I/O Registers and Conventional Memory

虽然硬件的IO register和memory看上去相似,但是IO reigster 的访问还是有一些特殊的地方。比如相对于普通的内存读写,IO的reigster读写会有副作用。CPU为了性能考虑,通常会对不会产生副作用的code进行优化,比如使用cache,以及指令乱序。在使用cache的情况下,CPU对某个变量的读写可能只是操作从cache,而不是变量对应的真正的physical memory。指令乱序,可能发生在编译阶段,也可能发生在硬件执行阶段,指令乱序对于硬件来说可以获得更高的performance。

正是因为CPU或者编译器会对指令进行的优化,可能导致读写register的顺序发生变化,最终和driver期待的行为不一致,从而出现问题。解决cache问题比较容易:只要底层硬件禁止对IO region(IO port或者内存地址访问)的访问进行cache就可以了;解决指令乱序的问题,需要driver自己调用kernel的一些接口来保证。kernel提供了一些函数来保证代码执行的时序:

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

barrier():告诉编译器,此处插入一个memory的barrier,但是对hardware不起作用。加了barrier之后,compiler编译出的指令会把所有当前被修改的value写入到memory里面去,并且也存放在CPU的cache里,后续需要的时候再读。在barrier前后的代码不会被编译器乱序优化,但是hardware仍然可以按照它自己的顺序执行。

#include <asm/system.h>
//插入一个hardware的read barrier,保证再次之前的read全部完成。
void rmb(void);
//插入一个hardware的write barrier,保证再次之前的write全部完成。
void wmb(void);
//插入一个hardware的barrier,保证再次之前的read和write全部完成。
void mb(void);
//这个读比较特殊,也是read barrier的一种,但是只会把依赖于别人read数据的read做barrier。
void read_barrier_depends(void);

上面的几个函数,和barrier不同的,他们在编译出的指令里添加了一些hardware memory barrier,实际的指令可能依赖于具体的硬件实现。rmb:保证在rmb之前的读操作在rmb之后的读操作执行前全部完成。wmb:保证在wmb之前的写操作在wmb之后的写操作执行前全部完成。mb:mb同时完成rmb和wmb两个功能。这三个函数都是barrier的超集,也就说他们也都包含了barrier的功能。

//下面的几个只有SMP上才有效,否则就是一个barrier()。
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);

上面的几个函数,只有当kernel编译成支持SMP才会有效,否则等同于barrier。

device driver中一个典型的barrier的用法可能是这样的:

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);

上面的例子中,为了保证所有的必要条件都ready(所有必要的寄存器已经全部写到hardware),使用一次wmb,保证所有的register都会写进去,然后kick off hardware,让hardware开始干活。

要注意,barrier是会影响performance的,只有确实需要的时候才加,并且要准确的使用barrier,比如挑选合适的barrier type等。例如,在x86架构上,当前wmb什么也不做,因为CPU默认不会对写操作进行指令乱序,所以wmb不需要做任何事;但是rmb不一样的,它会真的阻止CPU做指令乱序,所以从performance上看,会发现wmb比rmb要快。

另外,kernel中的一些同步原语也会起到barrier的作用,比如spinlock和atomic_t。kernel还提供了赋值加barrier的操作:

#include <asm/system.h>
#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

9.2. Using I/O Ports


在以前,很多device通过I/O Ports来访问寄存器。

9.2.1. I/O Port Allocation

首先,在使用port之前,要保证你是排他的访问,port不可以共享:

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

调用这个函数,就是告诉kernel你要使用n个ports,第一个从first开始,name就是device的name,返回非零值表明获取IO port成功,如果返回NULL,driver不可以使用这个IO port。kernel中所有被打开的port都可以在/proc/ioports这里看到。

如果不需要port了(一般是在driver unload的时候),需要释放IO port:

void release_region(unsigned long start, unsigned long n);

其实在获取port资源之前,可以向kernel query,查看port是否可用:

int check_region(unsigned long first, unsigned long n);

但是这个函数已经废弃不用了,因为查询到可用,不代表等会儿你去分的时候仍然可用。

9.2.2. Manipulating I/O ports

在获取到port资源以后,就会读或者写这些port。对大多数device而言,读写可能是8/16/32 bit的,也就是每次读写的数据大小是固定的,并且需要调用不同的函数,这些函数不可以混用。有些架构不支持IO port,只支持memory access,kernel针对这些架构,会将IO port操作,map成对memory的access,并向上提供了统一的接口。这些接口有:

//byte的形式读写
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);

//word的形式读写
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);

//longword的形式读写,一般是32bit
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);

要注意的是,所有的IO port操作,最多就是32bit,即便是在64bit的机器上,IO的读写最多也只有32bit。

9.2.3. I/O Port Access from User Space

在user mode来访问IO port,这里不讲了。因为IO port用的没那么普遍,所以重点还是放在IO memory。

9.4. Using I/O Memory


I/O port的方式访问寄存器在以前比较常用,现在则使用I/O memory的方式。I/O memory包含两种:1,memory mapped register;2,device memory,这两种方式对于software来说没有什么区别,都是透明的。

I/O memory is simply a region of RAM-like locations that the device makes available to the processor over the bus. 按照LDD3里的定义,I/O memory类似于RAM,但又不是RAM,它是在device里的,并且通过bus可以被CPU读写。按照这种定义,I/O memory(register 和device memory)存在于device内部,并且通过bus暴露出来,CPU通过地址可以对I/O memory直接访问。

对I/O memory的访问方式,依赖于计算机架构,总线,以及使用的外设。这里将主要讨论ISA设备和PCI设备。

IO memory可以通过page 访问,也许不能,这取决于计算机platform或者使用的bus的不同。如果想通过page table的方式来访问,就需要kernel实现为I/O memory分配物理地址,并且通过ioremap对物理地址空间做过map,这样kernel的page table就有entry,从而可以像访问普通的memory一样访问IO memory。如果没有page table,只能使用类似于I/O port的方式来访问,也就是使用kernel提供的wrapper过的函数操作。

无论是否使用了ioremap,device driver都要尽量避免直接使用指向I/O memory的指针,虽然在hardware层面,I/O memory编址和物理内存是一样的,但是访问I/O memory时,还是有些区别要注意,在9.1.1中已经有所描述。推荐使用kernel里面的interface来访问。

9.4.1. I/O Memory Allocation and Mapping

I/O memory region必须先创建,才能使用:

#include <linux/ioport.h>
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);

这个函数分配了一段I/O memory region,地址从start开始,大小为len byte,name就是device name。如果成功,返回non-NULL指针,如果失败,返回NULL。所有已经分配的IO memory在/proc/iomem下都可以看到。当I/O region不再使用时,需要释放:

void release_mem_region(unsigned long start, unsigned long len);

同样的,I/O memory region也有一个query的接口,已经废弃不用:

int check_mem_region(unsigned long start, unsigned long len);

在使用I/O memory region之前,需要确保这段地址kernel可以直接访问。其实在很多系统上,通过request_mem_region拿到的memory并不能直接被kernel访问,在访问之前,还需要做remap,下面的接口就是专门为访问I/O memory region而设计的,它会为I/O memory region分配一段虚拟地址:

#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void * addr);

在map之后,kernel就可以访问到这段I/O memory region。但是,再次强调,不推荐直接使用ioremap返回的指针访问I/O memory region,而应该使用kernel提供的函数,这些函数在后面会列出来。

在上面ioremap的几个函数里,有一个ioremap_nocache,按照kernel的描述:"It's useful if some control registers are in such an area, and write combining or read caching is not desirable." 实际上,这个函数的实现在大部分系统上和ioremap是一样的。

9.4.2. Accessing I/O Memory

ioremap会返回指针地址,但是kernel不推荐直接使用,应该使用wrapper:

//这里使用的addr,是ioremap返回的地址(可以加上offset)

//读
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
//写
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);

//如果要读写比较多的数据,可以使用repeate版本。
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);

//一次读写一个block
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);

9.4.3. Ports as I/O Memory

有些hardware比较有意思,有些版本使用I/O port,有些版本使用I/O memory,暴露给kernel的register都是一样的,但是访问方式却不一样。为了减少这种差别造成的影响,kernel提供了一个函数:

void *ioport_map(unsigned long port, unsigned int count);

kernel会把count个port map出来,像一段I/O memory region一样。并且以后访问时,可以使用和I/O memory一样的访问方式。当I/O port不再使用时,要unmap:

void ioport_unmap(void *addr);

当然,在使用I/O port之前要request_io_region才能访问I/O port。

9.4.4. Reusing short for I/O Memory

这里的short是LDD3的一个sample code,这里不再详述了。

9.4.5. ISA Memory Below 1 MB

kernel里有一段ISA的I/O memory region,地址在640K-1M,也就是0xA0000-0x100000之间,size是384KB。这段region位于RAM内部,主要是由于历史的原因,在1980s,640KB被认为是谁也不会用到的地址空间,因为它太大了。kernel在bootup的时候会把这段region做map,然后通过虚拟地址访问,但是如果device driver需要访问,仍然要通过ioremap的方式获取虚拟地址,然后访问。

这里是一个例子:

#define ISA_BASE    0xA0000
#define ISA_MAX     0x100000  /* for general memory access */

    /* this line appears in silly_init */
    io_base = ioremap(ISA_BASE, ISA_MAX - ISA_BASE);


case M_8: 
  while (count) {
      *ptr = ioread8(add);
      add++;
      count--;
      ptr++;
  }
  break;


case M_32: 
  while (count >= 4) {
      iowrite8(*(u32 *)ptr, add);
      add += 4;
      count -= 4;
      ptr += 4;
  }
  break;

case M_memcpy:
  memcpy_fromio(ptr, add, count);
  break;

//用完之后
iounmap(io_base);

9.4.6. isa_readb and Friends

kernel提供了一些可以直接访问ISA的接口,比如isa_readb等等,但是kernel以后可能会删除,不推荐使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值