11.4 设备I/O端口和I/O内存的访问
设备通常会提供一组寄存器来控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器和状态寄存器。这些寄存器可能位于I/O空间中,也可能位于内存空间中。当寄存器位于I/O空间时,被称为I/O端口;当寄存器位于内存空间时,对应的内存空间被称为I/O内存。
11.4.1 Linux I/O端口和I/O内存访问接口
1.I/O端口
在Linux设备驱动中,使用Linux内核提供的函数来访问定位于I/O空间的端口。
#include <asm/io.h>
1)读写字节端口(8位宽)。
unsigned inb(unsigned port); //读
void outb(unsigned char byte, unsigned port);//写
2)读写字端口(16位宽)。
unsigned inw(unsigned port);//读void outw(unsigned short word, unsigned port);//写
3)读写长字端口(32位宽)。
unsigned inl(unsigned port);//读
void outl(unsigned longword, unsigned port);//写
4)读写一串字节。
void insb(unsigned port, void *addr, unsigned long count);//读
insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存
void outsb(unsigned port, void *addr, unsigned long count);//写
将addr指向的内存中的count个字节连续写入以port开始的端口。
5)读写一串字。
void insw(unsigned port, void *addr, unsigned long count);//读
void outsw(unsigned port, void *addr, unsigned long count);//写
6)读写一串长字。
void insl(unsigned port, void *addr, unsigned long count);//读
void outsl(unsigned port, void *addr, unsigned long count);//写
上述各函数中I/O端口号port的类型长度依赖于具体的硬件平台。
2.I/O内存
在内核中访问I/O内存(通常是芯片内部的各个I2C、SPI、USB等控制器的寄存器或者外部内存总线上的设备)之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址上。
ioremap()的原型:
void *ioremap(unsigned long offset, unsigned long size);
ioremap()与vmalloc()类似,也需要建立新的页表,但是ioremap()并不进行vmalloc()中所执行的内存分配行为。ioremap()返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围,这个虚拟地址位于vmalloc映射区域。通过ioremap()获得的虚拟地址应该被iounmap()函数释放,其原型如下:
void iounmap(void * addr);
ioremap()有个变体是devm_ioremap(),通过devm_ioremap()进行的映射通常不需要在驱动退出和出错处理的时候进行iounmap()。
#include <linux/io.h>
devm_ioremap()的原型为:
void __iomem *devm_ioremap(struct device *dev, resource_size_t offset, unsigned long size);
在设备的物理地址(一般都是寄存器)被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是Linux内核推荐用一组标准的API来完成设备内存映射的虚拟地址的读写。
读寄存器用readb_relaxed()、readw_relaxed()、readl_relaxed()、readb()、readw()、
readl()这一组API,以分别读8bit、16bit、32bit的寄存器,没有_relaxed后缀的版本与有_relaxed后缀的
版本的区别是没有_relaxed后缀的版本包含一个内存屏障,如:
#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c) ({ u16__v = readw_relaxed(c); __iormb(); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })
写寄存器用writeb_relaxed()、writew_relaxed()、writel_relaxed()、writeb()、writew()、
writel()这一组API,以分别写8bit、16bit、32bit的寄存器,没有_relaxed后缀的版本与有_relaxed后缀的
版本的区别是没有_relaxed后缀的版本包含一个内存屏障,如:
#define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })
11.4.2 申请与释放设备的I/O端口和I/O内存
1.I/O端口申请
Linux内核提供一组函数以申请和释放I/O端口,表明该驱动要访问这片区域。
#include <linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n, const char *name);// 申请IO端口
这个函数向内核申请n个端口,这些端口从first开始,name参数为设备的名称。如果分配成功,则返回值不是NULL,如果返回NULL,则意味着申请端口失败。
当用request_region()申请的I/O端口使用完成后,应当使用release_region()函数将它们归还给系统,这个函数的原型如下:
void release_region(unsigned long start, unsigned long n); //释放IO端口
2.I/O内存申请
Linux内核提供一组函数用来申请和释放I/O内存的范围。此处的“申请”表明该驱动要访问这片区域,它不会做任何内存映射的动作。
#include <linux/ioport.h>
struct resource *request_mem_region(unsigned long first, unsigned long n, char *name);// 申请IO内存
这个函数向内核申请n个内存地址,这些地址从first开始,name参数为设备的名称。如果分配成功,则返回值不是NULL,如果返回NULL,则意味着申请I/O内存失败。
当用request_mem_region()申请的I/O内存使用完成后,应当使用release_mem_region()函数将它们归还给系统,这个函数的原型如下:
void release_mem_region(unsigned long start, unsigned long len);// 释放IO内存
request_region()和request_mem_region()也分别有变体,其为devm_request_region()和
devm_request_mem_region()。
11.4.3 设备I/O端口和I/O内存访问流程
1、设备驱动访问I/O端口的步骤
I/O端口访问的一种途径是直接使用I/O端口操作函数:在设备打开或驱动模块被加载时申请I/O端口区域,之后使用inb()、outb()等进行端口访问,最后,在设备关闭或驱动被卸载时释放I/O端口范围。
整个流程如图11.10所示。
图11.10 I/O端口访问流程
2、设备驱动访问I/O内存的步骤
1)调用request_mem_region()申请IO内存资源。
2)将设备寄存器的物理地址通过ioremap()映射到内核空间的虚拟地址。
3)通过Linux设备访问编程接口访问设备的寄存器。
4)访问完成后,调用iounmap()函数对ioremap()映射的虚拟地址解除映射,并调用release_mem_region()函数释放申请的I/O内存资源。