打印

[新手引导]linux系统内核中ioremap映射分析

[复制链接]
344|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
电员师|  楼主 | 2018-9-21 16:44 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,CPU对IO端口的编址方式有两种:

(1)I/O映射方式(I/O-mapped)

  典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。

(2)内存映射方式(Memory-mapped)

RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。

但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。

  一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源(也即是我们可以像读写RAM那样直接读写I/O内存(外设寄存器)资源了)。



  为了配置寄存器,我们需要知道寄存器在操作系统中的虚拟地址,因为驱动中要使用的是虚拟地址而非物理地址。在s3c2410中获得GPIO各个寄存器的方法有两种。

第一种是静态映射法:使用系统初始化已经设置好的虚拟地址,这种方法在操作系统启动过程中,页表已经生成,可以直接使用,这时候可以使用内核导出函数在arch/arm/plat-s3c24xx/Gpio.c

s3c2410_gpio_cfgpin  s3c2410_gpio_getcfg  s3c2410_gpio_setpin  s3c2410_gpio_getpin等函数配置寄存器GPIO引脚功能



以setpin函数为例:

void s3c2410_gpio_setpin(unsigned int pin, unsigned int to)

{

void __iomem *base = S3C24XX_GPIO_BASE(pin);

/*算出端口所在组虚拟基址*/

unsigned long offs = S3C2410_GPIO_OFFSET(pin);

/*算出端口所在组的偏移量(0-31)*/

unsigned long flags;

unsigned long dat;



local_irq_save(flags);

dat = __raw_readl(base + 0x04); /*基址 + 0x04为DAT寄存器

*/

dat &= ~(1 << offs);

dat |= to << offs;

__raw_writel(dat, base + 0x04);

local_irq_restore(flags);

}



分析代码:

void __iomem *base = S3C24XX_GPIO_BASE(pin);

//定义一个指向所要配置的控制寄存器的基地址的指针,并赋初值(是个虚拟地址)



/*

#define S3C24XX_GPIO_BASE(x)  S3C2410_GPIO_BASE(x)

#define S3C2410_GPIO_BASE(pin)   ((((pin)&~31)>>1)+  

S3C24XX_VA_GPIO)

#define S3C24XX_VA_GPIO   ((S3C24XX_PA_GPIO-S3C24XX_PA_UART)+  

S3C24XX_VA_UART)

#define S3C24XX_VA_UART    S3C_VA_UART

#define S3C_VA_UART S3C_ADDR(0x01000000)

#define S3C_ADDR(x)((void __iomem __force *)S3C_ADDR_BASE +  

(x))

#define S3C_ADDR_BASE (0xF4000000)

*/

__iomem是linux2.6.9内核中加入的特性。是用来个表示指针是指向一个I/O的内存空间。主要是为了驱动程序的通用性考虑。由于不同的CPU体系结构对I/O空间的表示可能不同。当使用__iomem时,编译器会忽略对变量的检查(因为用的是void __iomem)。若要对它进行检查,当__iomem的指针和正常的指针混用时,就会发出一些警告。

dat = __raw_readl(base + 0x04);//取原寄存器的值初始化dat

dat &= ~(1 << offs);  //掩码运算,清零对应位

dat |= to << offs;    //功能码生成,配置对应位的值

__raw_writel(dat, base + 0x04); //写入控制寄存器

__raw_readl和__raw_writel:

Linux对I/O的操作都定义在asm/io.h中,相应的在arm平台下,就在asm-arm/io.h中。

#define __raw_writel(v,a)(__chk_io_ptr(a), *(volatile unsigned int __force   *)(a) = (v))

#define __raw_readl(a)(__chk_io_ptr(a), *(volatile unsigned int __force   *)(a))



在include\linux\compiler.h中:

#ifdef __CHECKER__

……

extern void __chk_io_ptr(void __iomem *);

#else

……

# define __chk_io_ptr(x) (void)0

……

#endif

__raw_readl(a)展开是:((void)0, *(volatile unsigned int _force *)(a))。在定义了__CHECKER__的时候先调用__chk_io_ptr检查该地址,否则__chk_io_ptr什么也不做,* (volatile unsigned int _force *)(a)就是返回地址为a处的值。(void)xx的做法有时候是有用的,例如编译器打开了检查未使用的参数的时候需要将没有用到的参数这么弄一下才能 编译通过。CPU对I/O的物理地址的编程方式有两种:一种是I/O映射,一种是内存映射。 __raw_readl和__raw_writel等是原始的操作I/O的方法,由此派生出来的操作方法有:inb、outb、 _memcpy_fromio、readb、writeb、ioread8、iowrite8等。



静态映射方式下虚拟地址的建立是由操作系统完成的,不需要用户进行干涉。在arch/arm/mach-s3c2440/mach-mini2440.c中,其中包含了io端口虚拟地址到物理地址映射的初始化函数:

MACHINE_START(S3C2440, "s3cARM s3c2440 development board")

。。。。。

.map_io = s3c2440_map_io,

。。。。。

MACHINE_END



static void __init s3c2440_map_io(void)

{

/*该函数完成静态的物理地址到虚拟地址的转换*/

s3c24xx_init_io(s3c2440_iodesc, ARRAY_SIZE(s3c2440_iodesc));

s3c24xx_init_clocks(12000000);

s3c24xx_init_uarts(s3c2440_uartcfgs, ARRAY_SIZE(s3c2440_uartcfgs));

}



第二种是动态映射法,通过ioremap函数获得相应的虚拟地址。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,原型如下:

void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

iounmap函数用于取消ioremap()所做的映射,原型如下:

void iounmap(void * addr);

这两个函数都是实现在arch/XXX/mm/ioremap.c文件中。

    我们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。

  在Linux源代码中进行包含"ioremap"文本的搜索,发现真正出现的ioremap的地方相当少。所以笔者追根索源地寻找I/O操作的物理地址转换到虚拟地址的真实所在,发现Linux有替代ioremap的语句,但是这个转换过程却是不可或缺的。有没有出现ioremap是次要的,关键问题是有无虚拟地址和物理地址的转换!

  下面的程序在启动的时候保留一段内存,然后使用ioremap将它映射到内核虚拟空间,同时又用remap_page_range映射到用户虚拟空间,这样一来,内核和用户都能访问。如果在内核虚拟地址将这段内存初始化串"abcd",那么在用户虚拟地址能够读出来:



/************mmap_ioremap.c**************/

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/errno.h>

#include <linux/mm.h>

#include <linux/wrapper.h> /* for mem_map_(un)reserve */

#include <asm/io.h> /* for virt_to_phys */

#include <linux/slab.h> /* 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映射到用户虚拟空间。这样内核空间和用户空间都能访问这段被映射后的虚拟地址。)

使用特权

评论回复

相关帖子

发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

471

主题

480

帖子

0

粉丝