linux 实例分析内核IO内存映射

在内核空间分配内存


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

一般的,用户空间使用函数malloc在堆上分配内存空间,同样的,在内核空间同样有一套类似的函数来分配空间。下面的知识会涉及页式管理的内存机制,如果不懂的要先复习一下,在S3C2440数据手册的MMU部分有介绍。

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


一、内核空间和用户空间有什么不同


c语言的时候应该学过,从用户空间看,每个进程都傻乎乎的以为自己有4G的内存空间,其中位于高地址(3G-4G)的1G空间给内核用,另外的3G0-3G)都是它一个人独占的。所以用户空间很慷慨的把3G的空间分了好几个区域,如堆、栈、代码段等。其中,malloc()分配的空间位于堆,而程序中的自动变量,如你在函数内定义的“int i”,它是放在栈上,同时。用户空间的栈是可变栈,即随着数据的增多,对应函数的栈空间也会增多。


跟每个用户空间的进程不一样,内核只有1G的空间,同时,除了自己本身有进程运行外,内核还要允许用户空间进程调用系统调用进入内核空间去执行。所以,内核对此相当吝啬,它规定在内核中的每个进程都只有4KB8KB32位下)的定长栈。出于这样的原因,大的数据结构就不能在栈中分配,只能请求内核分配新的空间来存放数据,如函数kmalloc()


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


二、内存的基本单位是字节吗?


在介绍分配内存空间的函数前,我们还要了解一下内存是怎么被划分的。


内核不仅知道用户空间中看到的1G内核空间是假的,它还知道实际的物理内存是多少(我的开发板是64M)。所以,内核的其中一个任务就是,当这段虚假内存中的数据需要调用时,内核把这段虚拟内存与实际的物理内存对应上,运行完后又把两段内存的对应关系撤销掉给另外的虚拟内存用。


既然知道虚拟内存与物理内存的关系,那它们是怎么对应的,难道是一个一个字节?如果这样子做的话内核肯定觉得崩溃。


页是内存管理的基本单位。内存管理器(MMU,用于虚拟地址与物理地址之间的转换)通常以页为单位进行出来。页是内存管理的最小单位。在32位的系统中,一页的大小为4KB。所以,64M的物理内存将被分为16384个页。每一个物理页对应地用一个struct page来维护,注意,该结构体是用来维护物理页,而不是虚拟也,结构体记录该页是否被使用,对应的虚拟地址是多少等信息。


由于内存访问的限制,内核又把内存分成了3个区。

如有些硬件的访问只能在24位的地址空间寻址,出于这总访问限制,linux把前16MB划分为ZONE_DMA——用于直接内存访问(MDA)。

x86体系里,高于896M的内存空间称为高端内存,这段内存区域的页和普通的内存页操作后有差异,这段区域划分为ZONE_HIGHMEM

剩下的,加载这两段区域之间的就是我们平时用的普通内存区域——ZONE_NORMAL


这这里要注意一下:

1)这些分区是指linux自己分的,当然,如果普通分区不够用,当然也可以占用其他区的空间。

2)分区的大小是根据体系结构而定的,一般的ARM下,ZONE_NORMAL就是所有的可用内存区域。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


三、分配内存时使用的标记gfp_mask


在讲如何分配内存之前,先讲一下分配内存时将会用到的gfp_mask。简单地讲,这个标记指定了分配内存时的要求。具体分三类:

行为修饰符:表示内核应当如何分配内存,如指定不能休眠等。

区修饰符:指定内存将要分配到上面讲的三个区中的哪一个。

类型标记:这包含了上面两种修饰符(或运算),这些标记是为了让用户更好地去使用。


标记有很多,我这里不一一介绍,需要的可以自己查阅《linux内核设计与实现(第三版)》P238页。这里我讲两个常用的类型标记:

1GFP_KERNEL:最常用的标记,用于可睡眠的进程上下文。

2GFP_ATOMIC:使用了这个标记,内存分配函数不会引起随眠

3GFP_USER:当需要给用户空间分配内存空间时使用该标记。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


四、分配内存的第一种方法——按页分配


这是内核提供的一种请求内存的底层机制,都是以页为单位分配内存。以下函数包含在<linux/gfh.h>


这分为两个步骤:

1、请求内核分配页,获得物理页对应的结构体struct page

static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)

使用:

该函数用于申请(1<<other)——2other次方个连续物理页,gfp_mask用于指定分配的方式,一般使用GFP_KERNELGFP_ATOMIC注意:函数会引起睡眠

返回值:

成功返回一个指针,指向这连续物理页的第一个struct page结构体,失败返回NULL

2、分配页后还不能直接用,需要得到该页对应的虚拟地址:

void *page_address(struct page *page)

其实这个函数就是获取page的成员virtual,但千万不要直接访问,需要使用这个函数。函数返回的是物理页对应的虚拟地址,注意,如果你申请了多个物理页,分配的物理页是连续的,对应的虚拟地址也是连续的。


上面的两个步骤其实可以合成一个函数:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)

这个函数的传参和alloc_pages的一样,不过它直接返回申请的物理页对应的虚拟地址。


当然,无论使用上面的哪种方法,当内存不用时,需要调用函数释放:

1、如果你使用上面的第一种方法:

void __free_pages(struct page *page, unsigned int order)

2、如果你使用的是第二种方法:

void free_pages(unsigned long addr, unsigned int order)


下面来个程序:

/*5th_mm/5th_mm_1/1st/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <linux/mm.h>

5

6 struct page *p;

7 char *s;

8

9 static int __init test_init(void) //模块初始化函数

10 {

11 unsigned long virt, phys;

12

13 #define SWITCH 0 //通过定义这个来切换校验这两种不同的方法

14 #if SWITCH

15 //alloc 2 pages

16 p = alloc_pages(GFP_KERNEL, 1);

17 if (NULL == p){ //必须检验错误

18 printk("alloc page error!\n");

19 return - ENOMEM;

20 }

21 s = page_address(p);

22 #else

23 s = (char *)__get_free_pages(GFP_KERNEL, 1);

24 if (NULL == s){

25 printk("alloc page error!\n");

26 return - ENOMEM;

27 }

28 #endif

29

30 phys = __pa((unsigned long)s); //通过虚拟地址获得对应的物理地址

31 virt = (unsigned long)__va(phys); //通过物理地址获得对应的虚拟地址

32 printk("<p->virtual, s>[%p]\n", s); //打印获得的虚拟地址

33 printk("<phys>[%p]\n", (void *)phys); //打印对应的物理地址

34 printk("<virt>[%p]\n", (void *)virt); //再打印虚拟地址,其实就是分配函数返回的地址

35

36 memcpy(s, "hello mm", 20);

37

38 printk("hello kernel\n");

39 return 0;

40 }

41

42 static void __exit test_exit(void) //模块卸载函数

43 {

44 #if SWITCH

45 __free_pages(p, 1);

46 #else

47 free_pages((unsigned long)s, 1);

48 #endif

49

50 printk("good bye kernel\n");

51 }

52

53 module_init(test_init);

54 module_exit(test_exit);

55

56 MODULE_LICENSE("GPL");

57 MODULE_AUTHOR("xoao bai");

58 MODULE_VERSION("v0.1");

再检验一下:

[root: 1st]# insmod test.ko

<p->virtual, s>[c3968000] //虚拟地址

<phys>[33968000] //对应的实际地址

<virt>[c3968000]

hello kernel [hello mm] //打印出来了,hello美眉!


上面我分配了两页后什么也没做,当然,如果你要是只分配一页,内核有贴心函数:

#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

#define __get_free_page(gfp_mask) __get_free_pages((gfp_mask),0)

另外还有一个函数,不仅给你分配一页空间,还帮你清零了,特别适用于给用户空间分配内存。

nsigned long get_zeroed_page(gfp_t gfp_mask)


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


五、分配内存的第二种方法——kmalloc()


kmalloc()的用法和malloc差不多,只是多了一个我前面介绍的标志gfp_flag

上函数,需要包含头文件<linux/slab.h>

void *kmalloc(size_t size, gfp_t flags)

成功返回指向这块内存的地址(虚拟地址),失败返回NULL。这里注意一下,返回的内存大小不一定是size,因为内存的分配是基于页来分配的,有时需要地址对齐之类,所有分配的内存地址可能比size大。函数同样会引起睡眠,如果不能睡眠需要使用GFP_ATOMIC

分配的内存必须释放,使用函数:

void kfree(const void *objp)


上个程序:

/*5th_mm_1/2nd/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <linux/mm.h>

5

6 char *s;

7

8 static int __init test_init(void) //模块初始化函数

9 {

10 s = kmalloc(20, GFP_KERNEL);

11 memcpy(s, "hello mm", 20);

12

13 printk("hello kernel [%s]\n", s);

14 return 0;

15 }

16

17 static void __exit test_exit(void) //模块卸载函数

18 {

19 kfree(s);

20 printk("good bye kernel\n");

21 }

22

23 module_init(test_init);

24 module_exit(test_exit);

25

26 MODULE_LICENSE("GPL");

27 MODULE_AUTHOR("xoao bai");

28 MODULE_VERSION("v0.1");

再验证一下:

[root: 2nd]# insmod test.ko

hello kernel [hello mm] //又打印出来了


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


六、其他的内存分配函数——vmalloc


有时候,内核不一定会有很大的一块连续物理内存,这时候kmalloc就不能处理这种情况了,它只能是分配连续的物理内存。需要用以下的函数vmalloc

分配:

void * vmalloc(unsigned long size);

同样的,成功返回首地址,失败返回NULL,切记这个函数会引起睡眠。而且没有标志可选。

释放:

void vfree(void *addr);

当指定的size没有真够大的连续空间时,这个函数就会像捡破烂一样,东捡一块西捡一块,凑成满足大小的物理内存,并连在一起形成连续的虚拟内存再把首地址返回,所以这个函数的操作很麻烦,出于性能的考虑,能不用的话尽量不用。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


七、slab


为了方便一些频繁被使用的数据,内核有了slab层的概念。大概意思就是,在告诉内存空间里面,内核定义多了slab(其实就是一页),这些页可以预先定义成用来存放什么数据。这样的话就方便了,当有这样的数据要存放,进程就可以申请放在slab层,这样的话就省去了内存分配和释放的操作。具体的介绍请看《linux内核设计与实现(第三版)》P245


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx




2.操作硬件——IO内存


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

在之前章节的驱动,都没有对硬件进行操作,接写来将从我之前学的裸板驱动开始,讲解在linux系统下如何访问硬件。

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


一、IO端口与IO内存


介绍之前可以看看以下的博客:http://blog168.chinaunix.net/link.php?url=http://blogold.chinaunix.net%2Fu2%2F66435%2Fshowart_2137870.html


x86体系和ARM体系的寻址方式是有差别的:

x86下,为了能够满足CPU高速地运行,内存与CPU之间通过北桥相连并通过地址方式访问,而外设通过南桥与CPU相连并通过端口访问。

ARM下也实现了类似的操作,通过两条不同的总线(AHB BUSAPB BUS)来连接不同访问速度的外设。但是它与x86不同,无论是内存还是外设,ARM都是通过地址访问。

因为这两种访问方式的不同,linux分出了两种不同的访问操作:

以地址方式访问硬件——使用IO内存操作。

以端口方式访问硬件——使用IO端口操作。


ARM下,访问寄存器就像访问内存一样——从指定的寄存器地址获取数据,修改。所以,ARM下一般是使用IO内存的操作。但这并不是说IO端口的操作在ARM下不能用,它们的代码差不多,只是没有使用的必要,下面也将介绍IO内存操作。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


二、如何使用IO内存获得硬件的地址


之前已经说过,不能在linux使用实际的物理地址,要对指定的物理地址进行操作,必须要先将物理地址与虚拟地址对应,通过虚拟地址访问。于是有了以下的物理地址映射函数:

#include <asm/io.h>

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

其实这也是上一节介绍的内存分配的一种方式,它同样会建立新页表来管理虚拟地址。函数传入两个参数,需要访问的物理内存(寄存器)的首地址phys_addr和这段内存区域的大小size,返回与该段物理地址对应的虚拟地址。这段地址可以多次被映射,当然,每次映射的虚拟地址也不一样。

对应的也有撤销映射关系的函数:

void ioumap(void *addr);


接下来,我将会从一个裸板的ARMled驱动开始,讲解linux下的操作和裸板有什么不一样。

我的ARM裸板程序是在linux下编写的,我不知道这跟win下使用ADS有什么区别,在裸板驱动中,一般我是通过这样的办法来操作寄存器的:

首先,先给个地址定义个容易记的名字:

#define GPECON *(volatile unsigned long *) 0x56000040

接着,我就要操作这个GPECON寄存器了:

*GPECON &= ~(3 << 24); //2425位清零

*GPECON |= (1 << 24); //2425位分别赋值为10

可以看到,操作寄存器其实就是拿个地址出来进行操作。其实在linux下也是一样,只是操作的时候不能使用物理地址,需要用映射出来的虚拟地址

上个函数,这个程序我将要点亮连在我开发板上的led灯,这个灯接在我开发板的GPE12上,如果需要下载程序运行,需要改一下接口

/*5th_mm_2/1st/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <asm/io.h> //上面介绍的函数需要包含该头文件

5

6 volatile unsigned long virt, phys; //用于存放虚拟地址和物理地址

7 volatile unsigned long *GPECON, *GPEDAT, *GPEUP; //用与存放三个寄存器的地址

8

9 void led_device_init(void)

10 {

11 phys = 0x56000000; //1、指定物理地址

12 virt = (unsigned long)ioremap(phys, 0x0c); //2、通过ioremap获得对应的虚拟地址

13 //0x0c表示只要12字节的大小

14 GPECON = (unsigned long *)(virt + 0x40); //3、指定需要操作的三个寄存器的地址

15 GPEDAT = (unsigned long *)(virt + 0x44);

16 GPEUP = (unsigned long *)(virt + 0x48);

17 }

18

19 void led_configure(void) //led配置函数

20 {

21 *GPECON &= ~(3 << 24); //配置GPE12为输出端口

22 *GPECON |= (1 << 24); //先清零再赋值

23

24 *GPEUP |= (1 << 12); //禁止上拉电阻

25 }

26

27 void led_on(void) //点亮led

28 {

29 *GPEDAT &= ~(1 << 12);

30 }

31

32 void led_off(void) //灭掉led

33 {

34 *GPEDAT |= (1 << 12);

35 }

36

37 static int __init test_init(void) //模块初始化函数

38 {

39 led_device_init();

40 led_configure();

41 led_on();

42 printk("hello led!\n");

43 return 0;

44 }

45

46 static void __exit test_exit(void) //模块卸载函数

47 {

48 led_off();

49 iounmap((void *)virt); //注意,即使取消了映射,通过之前的虚拟地址还能访问硬件,

50 printk("bye\n"); //但不是肯定可以,只要该虚拟地址被内核改动后就不行了。

51 }

52

53 module_init(test_init);

54 module_exit(test_exit);

55

56 MODULE_LICENSE("GPL");

57 MODULE_AUTHOR("xoao bai");

58 MODULE_VERSION("v0.1");

从上面的程序可以看到,除了获得地址有点和裸板驱动不一样外,寄存器的操作还是一样的

接下来验证一下:

[root: 1st]# insmod test.ko

hello led! //这时候灯亮了

[root: 1st]# rmmod test

bye //灯灭了


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


三、改进函数,使用更好的内存访问接口


为了实现更好的移植性,上面的程序就有缺陷了。内核建议,尽量使用内核提供的内存访问接口:

#include <asm/io.h>

//从内存读取数据,返回值是指定内存地址中的值

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)

一般常用的是32位内存存取接口。

接下来就改进一下函数,其实实质没有改变,上面的函数是根据对应的平台体系结构编写的,这样可以提高驱动的移植性。

/*5th_mm_2/1st/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <asm/io.h>

5 #include <asm/sizes.h>

6

7 volatile unsigned long virt, phys;

8 volatile unsigned long *GPECON, *GPEDAT, *GPEUP;

9 unsigned long reg;

10

11 void led_device_init(void)

12 {

13 phys = 0x56000000;

14 virt = (unsigned long)ioremap(phys, SZ_16); //这里只是想介绍一下,在asm/sizes.h中有一下

15 //定义好用来表示内存大小的宏,这里其实我只

16 GPECON = (unsigned long *)(virt + 0x40); //需要12个字节,并不需要16个字节。

17 GPEDAT = (unsigned long *)(virt + 0x44);

18 GPEUP = (unsigned long *)(virt + 0x48);

19 }

20

21 void led_configure(void)

22 {

23 //*GPECON &= ~(3 << 24);

24 //*GPECON |= (1 << 24);

25 reg = ioread32(GPECON);

26 reg &= ~(3 << 24);

27 reg |= (1 << 24);

28 iowrite32(reg, GPECON);

29

30 //*GPEUP |= (1 << 12);

31 reg = ioread32(GPEUP);

32 reg &= ~(3 << 12);

33 iowrite32(reg, GPEUP);

34 }

35

36 void led_on(void)

37 {

38 //*GPEDAT &= ~(1 << 12);

39 reg = ioread32(GPEDAT);

40 reg &= ~(1 << 12);

41 iowrite32(reg, GPEDAT);

42 }

43

44 void led_off(void)

45 {

46 //*GPEDAT |= (1 << 12);

47 reg = ioread32(GPEDAT);

48 reg |= (1 << 12);

49 iowrite32(reg, GPEDAT);

50 }

51

52 static int __init test_init(void) //模块初始化函数

53 {

54 led_device_init();

55 led_configure();

56 led_on();

57 printk("hello led!\n");

58 return 0;

59 }

60

61 static void __exit test_exit(void) //模块卸载函数

62 {

63 led_off();

64 iounmap((void *)virt);

65 printk("bye\n");

66 }

67

68 module_init(test_init);

69 module_exit(test_exit);

70

71 MODULE_LICENSE("GPL");

72 MODULE_AUTHOR("xoao bai");

73 MODULE_VERSION("v0.1");

会发现发现,程序将原来直接访问内存的一句话变成了3句话,其他都没有改变。

我就不验证了,效果其实是一样的。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


四、再改进一下程序:


在使用IO内存映射操作之前,其实还可以添加一个步骤:分配内存区域。

#include <ioport.h>

struct resource *request_mem_region(unsigned long start, unsinged long len, char *name)

该函数从start开始分配len字节长的内存空间。如果成功,返回一个结构体指针,但这结构体我们没必要用,如果失败返回NULL。成功后,可以在.proc/iomem查看到name的信息。

其实调用request_mem_region()不是必须的,但是建议使用。该函数的任务是检查申请的资源是否可用,如果可用则申请成功,并标志为已经使用,其他驱动想再申请该资源时就会失败。

如果不再使用,需要调用释放函数:

void release_mem_region(unsigned long start, unsigned long len)


现在把这两个函数加上去:

/*5th_mm_2/3rd/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <asm/io.h>

5 #include <linux/ioport.h>

6

7 volatile unsigned long virt, phys;

8 volatile unsigned long *GPECON, *GPEDAT, *GPEUP;

9 unsigned long reg;

10 struct resource *led_resource;

11

12 void led_device_init(void)

13 {

14 phys = 0x56000000;

15 virt = (unsigned long)ioremap(phys, 0x0c);

16

17 GPECON = (unsigned long *)(virt + 0x40);

18 GPEDAT = (unsigned long *)(virt + 0x44);

19 GPEUP = (unsigned long *)(virt + 0x48);

20 }

21

22 void led_configure(void)

23 {

24 reg = ioread32(GPECON);

25 reg &= ~(3 << 24);

26 reg |= (1 << 24);

27 iowrite32(reg, GPECON);

28

29 reg = ioread32(GPEUP);

30 reg &= ~(3 << 12);

31 iowrite32(reg, GPEUP);

32 }

33

34 void led_on(void)

35 {

36 reg = ioread32(GPEDAT);

37 reg &= ~(1 << 12);

38 iowrite32(reg, GPEDAT);

39 }

40

41 void led_off(void)

42 {

43 reg = ioread32(GPEDAT);

44 reg |= (1 << 12);

45 iowrite32(reg, GPEDAT);

46 }

47

48 static int __init test_init(void) //模块初始化函数

49 {

50 led_device_init();

51

52 led_resource = request_mem_region(phys, 0x0c, "LED_MEM");

53 if(NULL == led_resource){

54 printk("request mem error!\n");

55 return - ENOMEM;

56 }

57

58 led_configure();

59 led_on();

60 printk("hello led!\n");

61 return 0;

62 }

63

64 static void __exit test_exit(void) //模块卸载函数

65 {

66 if(NULL != led_resource){

67 led_off();

68 iounmap((void *)virt);

69 release_mem_region(phys, 0x0c);

70 }

71 printk("bye\n");

72 }

73

74 module_init(test_init);

75 module_exit(test_exit);

76

77 MODULE_LICENSE("GPL");

78 MODULE_AUTHOR("xoao bai");

79 MODULE_VERSION("v0.1");

写完就得验证一下:

[root: 3rd]# insmod test.ko

hello led! //灯亮了

[root: 3rd]# cat /proc/iomem

19000300-19000310 : cs8900

19000300-19000310 : cs8900

。。。。

56000000-5600000b : LED_MEM //看到了

57000000-570000ff : s3c2410-rtc

57000000-570000ff : s3c2410-rtc

5a000000-5a0fffff : s3c2440-sdi

[root: 3rd]# rmmod test

bye //灯灭了

[root: 3rd]# cat /proc/iomem //LED_MEM不见了

19000300-19000310 : cs8900

19000300-19000310 : cs8900

。。。。。。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

有时候会觉得,每次访问硬件都要先通过ioremap来获取虚拟地址,其实有没有一种一劳永逸的方法,只要一次的操作,以后就能通过这个地址来访问硬件。答案是“有”,这就是接下来要介绍的IO内存静态映射。

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


一、静态IO是怎么建立的


Io静态映射发生在内核启动的时候,接下来通过内核源代码来分析,如果你的开发板是mini2440或者时候mini2440的内核配置文件,可以跟着我同样修改。注意:我的开发板只是使用mini2440的配置文件,外围电路跟mini2440不一样。


:以下代码在内核目录linux-2.6.29/arch/arm/mach-s3c2440/mach-mini2440.c

静态映射的建立方法,是在内核启动的时候,读取struct map_desc结构体里面的成员:

/*arch/arm/include/asm/mach/map.h*/

14 struct map_desc {

15 unsigned long virtual; //存放以后需要操作的虚拟地址,由自己定义

16 unsigned long pfn; //需要操作的硬件的物理地址对应的页帧号,即物理地址右移12

17 unsigned long length; //需要映射的大小

18 unsigned int type; //类型

19 };

这里要说明两个成员:

1)物理地址的页帧号pfn:如果你了解linux的页式管理,那你应该知道,一个页的大小是4096B2 << 12),所以一个地址的31-12位是用来表示一个地址对应的页帧号。对应的,一个物理地址,只要右移12位就能得到对应的页帧号,也可以使用函数:

#define __phys_to_pfn(paddr) ((paddr) >> PAGE_SHIFT) //其实也是右移12

2)类型type:有下面这些类型定义:

/*include/asm/mach/map.h*/

21 /* types 0-3 are defined in asm/io.h */

22 #define MT_UNCACHED 4

23 #define MT_CACHECLEAN 5

24 #define MT_MINICLEAN 6

25 #define MT_LOW_VECTORS 7

26 #define MT_HIGH_VECTORS 8

27 #define MT_MEMORY 9

28 #define MT_ROM 10

其中,MT_UNCACHED是我们常用的表示该地址不放在缓冲区cached。要知道,为了方便内存的访问,内核会将一些经常使用的内存数据放在cached中,但是这样在访问寄存器时就不行了,如果寄存器改变了,内核读取数据是从cached中读取数据,而不在寄存器读取,这样的做法是不合理的。


首先,我们需要往这个结构体中填充我们需要访问的地址。

本来这个结构体是空的。

/*arch/arm/mach-s3c2440/mach-mini2440.c*/

45 static struct map_desc mini2440_iodesc[] __initdata = {

46 };

修改成:

45 static struct map_desc mini2440_iodesc[] __initdata = {

46 {

47 .virtual = 0xeeee0000,

48 .pfn = __phys_to_pfn(0x56000000), //0x56000

49 .length = SZ_4K, //这里我直接映射一页

50 .type = MT_UNCACHED

51 },

52 };


填充结构体后,我们再看看启动时通过调用什么函数:

/*linux-2.6.29/arch/arm/mach-s3c2440/mach-mini2440.c*/

264 static void __init mini2440_map_io(void)

265 { //就是这个函数,将我刚才修改的结构体的成员进行静态映射

266 s3c24xx_init_io(mini2440_iodesc, ARRAY_SIZE(mini2440_iodesc));

267 s3c24xx_init_clocks(12000000);

268 s3c24xx_init_uarts(mini2440_uartcfgs, ARRAY_SIZE(mini2440_uartcfgs));

269 }

这个函数里面有一个重要的函数——iotable_init(),其实大部分的工作都由这个函数来完成,实现静态映射。


既然修改了内核,就需要重新编译内核:

make bzImage


通过上面这几步,我们就实现了这样的一个操作,可以通过虚拟地址0xeeee0000来访问一页的物理地址。既然知道了虚拟地址和物理地址之间的关系,就不需要再用ioremap了。


为了更好的规范,我们使用一个头文件来定义寄存器的访问地址,方便编程时使用:

/*5th_mm_3/1st/test_map_io.h*/

1 #ifndef _TEST_H

2 #define _TEST_H

3

4 typedef volatile unsigned long * s3c_reg_t;

5

6 #define S3C2440_VA 0xeeee0000 //我们已经知道静态映射的虚拟地址

7

8 #define S3C2440_BASE(x) (S3C2440_VA + (x))

9 #define S3C2440_GPEBASE S3C2440_BASE(0x40)

10 #define S3C2440_GPECON S3C2440_BASE(0x40) //这就是我们要操作的寄存器

11 #define S3C2440_GPEDAT S3C2440_BASE(0x44)

12 #define S3C2440_GPEUP S3C2440_BASE(0x48)

13

14

15 #endif /* _TEST_H */

然后再修改一下前一节的2nd函数,去掉ioremap部分:

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <asm/io.h>

5 #include "test_map_io.h"

6

7 s3c_reg_t *GPECON, *GPEDAT, *GPEUP;

8 unsigned long reg;

9

10 void led_device_init(void)

11 {

12 GPECON = (s3c_reg_t *)S3C2440_GPECON;

13 GPEDAT = (s3c_reg_t *)S3C2440_GPEDAT;

14 GPEUP = (s3c_reg_t *)S3C2440_GPEUP;

15 }

16

17 void led_configure(void)

18 {

19 reg = ioread32(GPECON);

20 reg &= ~(3 << 24);

21 reg |= (1 << 24);

22 iowrite32(reg, GPECON);

23

24 reg = ioread32(GPEUP);

25 reg &= ~(3 << 12);

26 iowrite32(reg, GPEUP);

27 }

28

29 void led_on(void)

30 {

31 reg = ioread32(GPEDAT);

32 reg &= ~(1 << 12);

33 iowrite32(reg, GPEDAT);

34 }

35

36 void led_off(void)

37 {

38 reg = ioread32(GPEDAT);

39 reg |= (1 << 12);

40 iowrite32(reg, GPEDAT);

41 }

42

43 static int __init test_init(void) //模块初始化函数

44 {

45 led_device_init();

46 led_configure();

47 led_on();

48 printk("hello led!\n");

49 return 0;

50 }

51

52 static void __exit test_exit(void) //模块卸载函数

53 {

54 led_off();

55 printk("bye\n");

56 }

57

58 module_init(test_init);

59 module_exit(test_exit);

60

61 MODULE_LICENSE("GPL");

62 MODULE_AUTHOR("xoao bai");

63 MODULE_VERSION("v0.1");

除了红笔部分,和删除的ioremap相关函数,其他部分都没有改动,效果还是一样,加载灯亮,卸载灯灭。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值