大多数外围设备是通过读写设备上的寄存器完成的。 通常分为控制寄存器、状态寄存器、数据寄存器三大类,外围设备的寄存器通常是连续寻址的。 根据CPU体系结构,CPU有两种方法来寻址IO端口。
(1) I/O映射方式(I/O-mapped ) ) ) )。
通常,与X86处理器一样,为外围设备实现称为“I/O地址空间”或“I/O端口空间”的单独的地址空间,CPU通过专用I/O指令(例如X86的IN和OUT指令)在该空间中
)2)内存映射方式(内存映射) )。
RISC命令系统上的CPU(arm、PowerPC等)通常只实现一个物理地址空间,而外围设备上的I/O端口是内存的一部分。 在这种情况下,CPU可以像存储器单元一样访问外围设备I/O端口,而无需设置专用的外围设备I/O命令。
但是,两者的硬件实现差异对软件是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外围存储器统一视为“I/O存储器”资源。
通常,在系统运行时,外围设备I/O内存资源的物理地址是已知的,由硬件设计决定。 但是,CPU通常没有为这些已知外围设备I/O内存资源的物理地址预定义虚拟地址范围。 驱动程序必须映射到核心虚拟地址空间,然后根据映射获得的核心虚拟地址范围访问这些I/O内存资源,而不是直接从物理地址访问I/O内存资源。 Linux在io.h头文件中声明了一个函数ioremap ),用于将I/O内存资源的物理地址映射到核心虚拟地址空间(3GB-4GB )。 原型如下。
void * io remap (unsigned long phys _ addr,unsigned long size,unsigned long flags );
iounmap函数用于取消ioremap ()的映射,原型为:
语音地图(语音* Addr;
这两个函数都在mm/ioremap.c文件中实现。
将I/O内存资源的物理地址映射到核心虚拟地址后,理论上可以像RAM一样直接读写I/O内存资源。 为了保证驱动程序跨平台的可移植性,必须使用Linux上的特定函数访问I/O内存资源。 不应该用指向核心虚拟地址的指针访问。 在x86平台上,读/写I/O函数如下所示:
#definereadb(addr ) (* ) * (电压无符号化char * ) _io_virt ) )
#definereadw(addr ) (* ) * (电压不统一短整型* ) _io_virt ) )
#定义索引(addr ) (* ) *(volatile unsigned int * ) _io_virt ) )
#definewriteb(b,addr ) (* ) *(volatile unsigned char * ) _ io _ virt (addr )=(b ) )
#definewritew(b,addr ) (* ) *(volatile unsigned short * ) _ io _ virt (addr )=(b ) )
#definewritel(b,addr ) (* ) *(volatile unsigned int * ) _io_virt ) (addr )=(b ) )
#definememset_io(a,b,c ) memset ) _io_virt ),b ),c ) )
#definememcpy_fromio(a,b,c ) memcpy ) (a )、_io_virt )、(c ) )
#definememcpy_toio(a,b,c ) memcpy ) _io_virt,(b,) c ) )
最后,特别强调驱动程序中mmap函数的实现方法。 在mmap中映射设备意味着将用户空间中的一些地址与设备内存相关联,并且实际上是对设备的访问,只要程序在分配的地址范围内进行读取或写入。
笔者在Linux源代码中进行了包含“ioremap”文本的搜索,发现实际出现的ioremap位置相当少。 为此,笔者寻找I/O操作物理地址转换为虚拟地址的真实位置,发现Linux中有一个词可以代替ioremap,但这个转换过程是必不可少的。
例如,让我们再讨论一下名为S3C2410的ARM芯片实时时钟(RTC )驱动中的一小部分。
staticvoidget_RTC_time(intALM,struct rtc_time *rtc_tm ) ) ) ) )。
{
spin_lock_IRQ(RTC_lock;
if(ALM==1) {
RTC _ TM-TM _ year=(统一字符图) ALMYEAR Msk_RTCYEAR;
rtc_tm-tm_mon=(unsigned char ) ALMMON Msk_RTCMON;
RTC_TM-TM_mday=(unsignedchar ) a
LMDAY & Msk_RTCDAY;rtc_tm->tm_hour = (unsigned char)ALMHOUR & Msk_RTCHOUR;
rtc_tm->tm_min = (unsigned char)ALMMIN & Msk_RTCMIN;
rtc_tm->tm_sec = (unsigned char)ALMSEC & Msk_RTCSEC;
}
else {
read_rtc_bcd_time:
rtc_tm->tm_year = (unsigned char)BCDYEAR & Msk_RTCYEAR;
rtc_tm->tm_mon = (unsigned char)BCDMON & Msk_RTCMON;
rtc_tm->tm_mday = (unsigned char)BCDDAY & Msk_RTCDAY;
rtc_tm->tm_hour = (unsigned char)BCDHOUR & Msk_RTCHOUR;
rtc_tm->tm_min = (unsigned char)BCDMIN & Msk_RTCMIN;
rtc_tm->tm_sec = (unsigned char)BCDSEC & Msk_RTCSEC;
if (rtc_tm->tm_sec == 0) {
/* Re-read all BCD registers in case of BCDSEC is 0.
See RTC section at the manual for more info. */
goto read_rtc_bcd_time;
}
}
spin_unlock_irq(&rtc_lock);
BCD_TO_ttdjb(rtc_tm->tm_year);
BCD_TO_ttdjb(rtc_tm->tm_mon);
BCD_TO_ttdjb(rtc_tm->tm_mday);
BCD_TO_ttdjb(rtc_tm->tm_hour);
BCD_TO_ttdjb(rtc_tm->tm_min);
BCD_TO_ttdjb(rtc_tm->tm_sec);
/* The epoch of tm_year is 1900 */
rtc_tm->tm_year += RTC_LEAP_YEAR - 1900;
/* tm_mon starts at 0, but rtc month starts at 1 */
rtc_tm->tm_mon--;
}
I/O操作似乎就是对ALMYEAR、ALMMON、ALMDAY定义的寄存器进行操作,那这些宏究竟定义为什么呢?
#define ALMDAY bRTC(0x60)
#define ALMMON bRTC(0x64)
#define ALMYEAR bRTC(0x68)
其中借助了宏bRTC,这个宏定义为:
#define bRTC(Nb) __REG(0x57000000 + (Nb))
其中又借助了宏__REG,而__REG又定义为:
# define __REG(x) io_p2v(x)
最后的io_p2v才是真正"玩"虚拟地址和物理地址转换的地方:
#define io_p2v(x) ((x) | 0xa0000000)
与__REG对应的有个__PREG:
# define __PREG(x) io_v2p(x)
与io_p2v对应的有个io_v2p:
#define io_v2p(x) ((x) & ~0xa0000000)
可见有没有出现ioremap是次要的,关键问题是有无虚拟地址和物理地址的转换!
下面的程序在启动的时候保留一段内存,然后使用ioremap将它映射到内核虚拟空间,同时又用remap_page_range映射到用户虚拟空间,这样一来,内核和用户都能访问。如果在内核虚拟地址将这段内存初始化串"abcd",那么在用户虚拟地址能够读出来:
/************mmap_ioremap.c**************/
#include
#include
#include
#include
#include /* for mem_map_(un)reserve */
#include /* for virt_to_phys */
#include /* for kmalloc and kfree */
MODULE_PARM(mem_start, "i");
MODULE_PARM(mem_size, "i");
static int mem_start = 101, mem_size = 10;
static char *reserve_virt_addr;
static int major;
int mmapdrv_open(struct inode *inode, struct file *file);
int mmapdrv_release(struct inode *inode, struct file *file);
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma);
static struct file_operations mmapdrv_fops =
{
owner: THIS_MODULE, mmap: mmapdrv_mmap, open: mmapdrv_open, release:
mmapdrv_release,
};
int init_module(void)
{
if ((major = register_chrdev(0, "mmapdrv", &mmapdrv_fops)) < 0)
{
printk("mmapdrv: unable to register character device/n");
return ( - EIO);
}
printk("mmap device major = %d/n", major);
printk("high memory physical address 0x%ldM/n", virt_to_phys(high_memory) /
1024 / 1024);
reserve_virt_addr = ioremap(mem_start *1024 * 1024, mem_size *1024 * 1024);
printk("reserve_virt_addr = 0x%lx/n", (unsigned long)reserve_virt_addr);
if (reserve_virt_addr)
{
int i;
for (i = 0; i < mem_size *1024 * 1024; i += 4)
{
reserve_virt_addr[i] = 'a';
reserve_virt_addr[i + 1] = 'b';
reserve_virt_addr[i + 2] = 'c';
reserve_virt_addr[i + 3] = 'd';
}
}
else
{
unregister_chrdev(major, "mmapdrv");
return - ENODEV;
}
return 0;
}
/* remove the module */
void cleanup_module(void)
{
if (reserve_virt_addr)
iounmap(reserve_virt_addr);
unregister_chrdev(major, "mmapdrv");
return ;
}
int mmapdrv_open(struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT;
return (0);
}
int mmapdrv_release(struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT;
return (0);
}
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
if (size > mem_size *1024 * 1024)
{
printk("size too big/n");
return ( - ENXIO);
}
offset = offset + mem_start * 1024 * 1024;
/* we do not want to have this area swapped out, lock it */
vma->vm_flags |= VM_LOCKED;
if (remap_page_range(vma, vma->vm_start, offset, size, PAGE_SHARED))
{
printk("remap page range failed/n");
return - ENXIO;
}
return (0);
}
remap_page_range函数的功能是构造用于映射一段物理地址的新页表,实现了内核空间与用户空间的映射,其原型如下:
int remap_page_range(vma_area_struct *vma, unsigned long from, unsigned long to, unsigned long size, pgprot_tprot);
使用mmap最典型的例子是显示卡的驱动,将显存空间直接从内核映射到用户空间将可提供显存的读写效率。
(在内核驱动程序的初始化阶段,通过ioremap()将物理地址映射到内核虚拟空间;在驱动程序的mmap系统调用中,使用remap_page_range()将该块ROM映射到用户虚拟空间。这样内核空间和用户空间都能访问这段被映射后的虚拟地址。)