您的位置:首页 > 运维架构 > Linux

Linux内核访问外设I/O资源的方式

2011-06-16 16:04 309 查看




Linux内核访问外设I/O资源的方式

Linux

内核访问外设
I/O

资源的方式


Author: Dongas

Date: 08-08-02

我们知道默认外设
I/O
资源是不在
Linux
内核空间中的(如
sram
或硬件接口寄存器等),若需要访问该外设
I/O
资源,必须先将其地址映射到内核空间中来,然后才能在内核空间中访问它。

Linux
内核访问外设
I/O
内存资源的方式有两种:动态映射
(ioremap)
和静态映射
(map_desc)


一、动态映射
(ioremap)

方式


动态映射方式是大家使用了比较多的,也比较简单。即直接通过内核提供的
ioremap
函数动态创建一段外设
I/O
内存资源到内核虚拟地址的映射表,从而可以在内核空间中访问这段
I/O
资源。

Ioremap
宏定义在
asm/io.h
内:

#define ioremap(cookie,size)
__ioremap(cookie,size,0)

__ioremap
函数原型为
(arm/mm/ioremap.c)


void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);

phys_addr
:要映射的起始的
IO
地址

size
:要映射的空间的大小

flags
:要映射的
IO
空间和权限有关的标志

该函数返回映射后的内核虚拟地址
(3G-4G).
接着便可以通过读写该返回的内核虚拟地址去访问之这段
I/O
内存资源。

举一个简单的例子
: (
取自
s3c2410

iis
音频驱动
)

比如我们要访问
s3c2410
平台上的
I2S
寄存器
,
查看
datasheet
知道
IIS
物理地址为
0x55000000
,我们把它定义为宏
S3C2410_PA_IIS
,如下:

#define S3C2410_PA_IIS

(0x55000000)

若要在内核空间
(iis
驱动
)
中访问这段
I/O
寄存器
(IIS)
资源需要先建立到内核地址空间的映射
:

our_card->regs = ioremap(S3C2410_PA_IIS, 0x100);

if (our_card->regs == NULL) {

err = -ENXIO;

goto exit_err;

}

创建好了之后,我们就可以通过
readl(our_card->regs )

writel(value, our_card->regs)

IO
接口函数去访问它。

二、静态映射
(map_desc)

方式


下面重点介绍静态映射方式即通过
map_desc
结构体静态创建
I/O
资源映射表。

内核提供了在系统启动时通过
map_desc
结构体静态创建
I/O
资源到内核地址空间的线性映射表
(

page table)
的方式,这种映射表是一种一一映射的关系。程序员可以自己定义该
I/O
内存资源映射后的虚拟地址。创建好了静态映射表,在内核或驱动中访问该
I/O
资源时则无需再进行
ioreamp
动态映射,可以直接通过映射后的
I/O
虚拟地址去访问它。

下面详细分析这种机制的原理并举例说明如何通过这种静态映射的方式访问外设
I/O
内存资源。

内核提供了一个重要的结构体
struct machine_desc ,
这个结构体在内核移植中起到相当重要的作用
,
内核通过
machine_desc
结构体来控制系统体系架构相关部分的初始化。

machine_desc
结构体的成员包含了体系架构相关部分的几个最重要的初始化函数,包括
map_io, init_irq, init_machine
以及
phys_io , timer
成员等。

machine_desc
结构体定义如下:

struct
machine_desc {

/*

* Note! The first four elements are used

* by assembler code in head-armv.S

*/

unsigned
int
nr;
/* architecture number    */

unsigned
int
phys_io;
/* start of physical io    */

unsigned
int
io_pg_offst;

/* byte offset for io

* page tabe entry    */

const
char
*
name;
/* architecture name    */

unsigned
long
boot_params;
/* tagged list        */

unsigned
int
video_start;
/* start of video RAM    */

unsigned
int
video_end;
/* end of video RAM    */

unsigned
int
reserve_lp0 :
1;
/* never has lp0    */

unsigned
int
reserve_lp1 :
1;
/* never has lp1    */

unsigned
int
reserve_lp2 :
1;
/* never has lp2    */

unsigned
int
soft_reboot :
1;
/* soft reboot        */

void
(
*
fixup)
(
struct
machine_desc *
,

struct
tag *
,
char
*
*
,

struct
meminfo *
)
;

void
(
*
map_io)
(
void
)
;
/* IO mapping function    */

void
(
*
init_irq)
(
void
)
;

struct
sys_timer    *
timer;
/* system tick timer    */

void
(
*
init_machine)
(
void
)
;

}
;


这里的
map_io
成员即内核提供给用户的创建外设
I/O
资源到内核虚拟地址静态映射表的接口函数。
Map_io
成员函数会在系统初始化过程中被调用
,
流程如下:

Start_kernel -> setup_arch() --> paging_init() --> devicemaps_init()
中被调用

Machine_desc
结构体通过
MACHINE_START
宏来初始化。

注:
MACHINE_START
的使用及各个成员函数的调用过程请参考
:
http://blog.chinaunix.net/u2/60011/showart_1010489.html
用户可以在定义
Machine_desc
结构体时指定
Map_io
的接口函数,这里以
s3c2410
平台为例。

s3c2410 machine_desc
结构体定义如下:

/* arch/arm/mach-s3c2410/Mach-smdk2410.c */

MACHINE_START(
SMDK2410,
"SMDK2410"
)

/* @TODO: request a new identifier and switch

* to SMDK2410 */

/* Maintainer: Jonas Dietsche */

.
phys_io    =
S3C2410_PA_UART,

.
io_pg_offst    =
(
(
(
u32)
S3C24XX_VA_UART)
>
>
18)
&
0xfffc,

.
boot_params    =
S3C2410_SDRAM_PA +
0x100,

.
map_io        =
smdk2410_map_io,

.
init_irq    =
s3c24xx_init_irq,

.
init_machine    =
smdk2410_init,

.
timer        =
&
s3c24xx_timer,

MACHINE_END


如上
,map_io
被初始化为
smdk2410_map_io

smdk2410_map_io
即我们自己定义的创建静态
I/O
映射表的函数。在
Porting
内核到新开发板时,这个函数需要我们自己实现。

(
注:这个函数通常情况下可以实现得很简单,只要直接调用
iotable_init
创建映射表就行了,我们的板子内核就是。不过
s3c2410
平台这个函数实现得稍微有点复杂,主要是因为它将要创建
IO
映射表的资源分为了三个部分
(smdk2410_iodesc, s3c_iodesc
以及
s3c2410_iodesc)
在不同阶段分别创建。这里我们取其中一个部分进行分析,不影响对整个概念的理解。
)

S3c2410
平台的
smdk2410_map_io
函数最终会调用到
s3c2410_map_io
函数。

流程如下:
s3c2410_map_io -> s3c24xx_init_io -> s3c2410_map_io

下面分析一下
s3c2410_map_io
函数:

void
__init s3c2410_map_io(
struct
map_desc *
mach_desc,
int
mach_size)

{

/* register our io-tables */

iotable_init(
s3c2410_iodesc,
ARRAY_SIZE(
s3c2410_iodesc)
)
;

……

}


iotable_init
内核提供,定义如下:

/*

* Create the architecture specific mappings

*/

void
__init iotable_init(
struct
map_desc *
io_desc,
int
nr)

{

int
i;

for
(
i =
0;
i <
nr;
i+
+
)

create_mapping(
io_desc +
i)
;

}


由上知道,
s3c2410_map_io
最终调用
iotable_init
建立映射表。

iotable_init
函数的参数有两个:一个是
map_desc
类型的结构体,另一个是该结构体的数量
nr
。这里最关键的就是
struct map_desc

map_desc
结构体定义如下:

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

struct
map_desc {

unsigned
long
virtual
;
/* 映射后的虚拟地址 */

unsigned
long
pfn;
/* I/O资源物理地址所在的页帧号 */

unsigned
long
length;
/* I/O资源长度 */

unsigned
int
type;
/* I/O资源类型 */

}
;


create_mapping
函数就是通过
map_desc
提供的信息创建线性映射表的。

这样的话我们就知道了创建
I/O
映射表的大致流程为:只要定义相应
I/O
资源的
map_desc
结构体,并将该结构体传给
iotable_init
函数执行,就可以创建相应的
I/O
资源到内核虚拟地址空间的映射表了。

我们来看看
s3c2410
是怎么定义
map_desc
结构体的
(
即上面
s3c2410_map_io
函数内的
s3c2410_iodesc)


/* arch/arm/mach-s3c2410/s3c2410.c */

static
struct
map_desc s3c2410_iodesc[
]
__initdata =
{

IODESC_ENT(
USBHOST)
,

IODESC_ENT(
CLKPWR)
,

IODESC_ENT(
LCD)
,

IODESC_ENT(
TIMER)
,

IODESC_ENT(
ADC)
,

IODESC_ENT(
WATCHDOG)
,

}
;


IODESC_ENT
宏如下:

#define IODESC_ENT(x) { (unsigned long)S3C24XX_VA_##x, __phys_to_pfn(S3C24XX_PA_##x), S3C24XX_SZ_##x, MT_DEVICE }

展开后等价于:

static
struct
map_desc s3c2410_iodesc[
]
__initdata =
{

{

.
virtual
=
(
unsigned
long
)
S3C24XX_VA_ LCD)
,

.
pfn        =
__phys_to_pfn(
S3C24XX_PA_ LCD)
,

.
length    =
S3C24XX_SZ_ LCD,

.
type    =
MT_DEVICE

}
,

……

}
;


S3C24XX_PA_ LCD

S3C24XX_VA_ LCD
为定义在
map.h
内的
LCD
寄存器的物理地址和虚拟地址。在这里
map_desc
结构体的
virtual
成员被初始化为
S3C24XX_VA_ LCD

pfn
成员值通过
__phys_to_pfn
内核函数计算,只需要传递给它该
I/O
资源的物理地址就行。
Length
为映射资源的大小。
MT_DEVICE

I/O
类型,通常定义为
MT_DEVICE


这里最重要的即
virtual
成员的值
S3C24XX_VA_ LCD
,这个值即该
I/O
资源映射后的内核虚拟地址,创建映射表成功后,便可以在内核或驱动中直接通过该虚拟地址访问这个
I/O
资源。

S3C24XX_VA_ LCD
以及
S3C24XX_PA_ LCD
定义如下:

/* include/asm-arm/arch-s3c2410/map.h */

/* LCD controller */

#define S3C24XX_VA_LCD

S3C2410_ADDR(0x00600000)
//LCD
映射后的虚拟地址

#define S3C2410_PA_LCD

(0x4D000000)
//LCD
寄存器物理地址

#define S3C24XX_SZ_LCD

SZ_1M
//LCD
寄存器大小

S3C2410_ADDR
定义如下:

#define S3C2410_ADDR(x)

((void __iomem *)0xF0000000 + (x))

这里就是一种线性偏移关系,即
s3c2410
创建的
I/O
静态映射表会被映射到
0xF0000000
之后。
(
这个线性偏移值可以改,也可以你自己在
virtual
成员里手动定义一个值,只要不和其他
IO
资源映射地址冲突
,
但最好是在
0XF0000000
之后。
)

(
注:其实这里
S3C2410_ADDR
的线性偏移只是
s3c2410
平台的一种做法,很多其他
ARM
平台采用了通用的
IO_ADDRESS
宏来计算物理地址到虚拟地址之前的偏移。

IO_ADDRESS
宏定义如下:

/* include/asm/arch-versatile/hardware.h */

/* macro to get at IO space when running virtually */

#define IO_ADDRESS(x)
(((x) & 0x0fffffff) + (((x) >> 4) & 0x0f000000) + 0xf0000000) )

s3c2410_iodesc
这个映射表建立成功后,我们在内核中便可以直接通过
S3C24XX_VA_ LCD
访问
LCD
的寄存器资源。

如:
S3c2410 lcd
驱动的
probe
函数内

/* Stop the video and unset ENVID if set */

info-
>
regs.
lcdcon1 &
=
~
S3C2410_LCDCON1_ENVID;

lcdcon1 =
readl(
S3C2410_LCDCON1)
;
//read映射后的寄存器虚拟地址

writel(
lcdcon1 &
~
S3C2410_LCDCON1_ENVID,
S3C2410_LCDCON1)
;
//write映射后的虚拟地址


S3C2410_LCDCON1
寄存器地址为相对于
S3C24XX_VA_LCD
偏移的一个地址,定义如下:

/* include/asm/arch-s3c2410/regs-lcd.h */

#define S3C2410_LCDREG(x) ((x) + S3C24XX_VA_LCD)

/* LCD control registers */

#define S3C2410_LCDCON1

S3C2410_LCDREG(0x00)

到此,我们知道了通过
map_desc
结构体创建
I/O
内存资源静态映射表的原理了。总结一下发现其实过程很简单,一通过定义
map_desc
结构体创建静态映射表,二在内核中通过创建映射后虚拟地址访问该
IO
资源。

三、
I/O

静态映射方式应用实例


I/O
静态映射方式通常是用在寄存器资源的映射上,这样在编写内核代码或驱动时就不需要再进行
ioremap
,直接使用映射后的内核虚拟地址访问。同样的
IO
资源只需要在内核初始化过程中映射一次,以后就可以一直使用。

寄存器资源映射的例子上面讲原理时已经介绍得很清楚了,这里我举一个
SRAM
的实例介绍如何应用这种
I/O
静态映射方式。当然原理和操作过程同寄存器资源是一样的,可以把
SRAM
看成是大号的
I/O
寄存器资源。

比如我的板子在
0x30000000
位置有一块
64KB
大小的
SRAM
。我们现在需要通过静态映射的方式去访问该
SRAM
。我们要做的事内容包括修改
kernel
代码,添加
SRAM
资源相应的
map_desc
结构,创建
sram
到内核地址空间的静态映射表。写一个
Sram Module,

Sram Module
内直接通过静态映射后的内核虚拟地址访问该
sram


第一步:创建
SRAM
静态映射表

在我板子的
map_des
结构体数组
(xxx_io_desc
)内添加
SRAM
资源相应的
map_desc
。如下:

static
struct
map_desc xxx_io_desc[
]
__initdata =
{

…………

{

.
virtual
=
IO_ADDRESS(
XXX _UART2_BASE)
,

.
pfn        =
__phys_to_pfn(
XXX _UART2_BASE)
,

.
length        =
SZ_4K,

.
type        =
MT_DEVICE

}
,
{

.
virtual
=
IO_ADDRESS(
XXX_SRAM_BASE
)
,

.
pfn        =
__phys_to_pfn(
XXX_SRAM_BASE
)
,

.
length        =
SZ_4K,

.
type        =
MT_DEVICE

}
,

}
;



XXX_SRAM_BASE
为我板子上
SRAM
的物理地址
,
定义为
0x30000000
。我的
kernel
是通过
IO_ADDRESS
的方式计算内核虚拟地址的,这点和之前介绍的
S3c2410
有点不一样。不过原理都是相同的,为一个线性偏移
,
范围在
0xF0000000
之后。

第二步:写个
SRAM Module,

Module
中通过映射后的虚拟地址直接访问该
SRAM
资源

SRAM Module
代码如下:

/* Sram Testing Module */

……

static
void
sram_test(
void
)

{

void
*
sram_p;

char
str[
]
=
"Hello,sram!/n"
;

sram_p =
(
void
*
)
IO_ADDRESS (
XXX_SRAM_BASE)
;
/* 通过IO_ADDRESS宏得到SRAM映射后的虚拟地址 */

memcpy
(
sram_p,
str,
sizeof
(
str)
)
;
//将 str字符数组拷贝到sram内

printk(
sram_p)
;

printk(
"/n"
)
;

}

static
int
__init sram_init(
void
)

{

struct
resource *
ret;

printk(
"Request SRAM mem region ............/n"
)
;

ret =
request_mem_region(
SRAM_BASE,
SRAM_SIZE,
"SRAM Region"
)
;

if
(
ret =
=
NULL
)
{

printk(
"Request SRAM mem region failed!/n"
)
;

return
-
1;

}

sram_test(
)
;

return
0;

}

static
void
__exit sram_exit(
void
)

{

release_mem_region(
SRAM_BASE,
SRAM_SIZE)
;

printk(
"Release SRAM mem region success!/n"
)
;

printk(
"SRAM is closed/n"
)
;

}

module_init(
sram_init)
;

module_exit(
sram_exit)
;


在开发板上运行结果如下:

/ # insmod bin/sram.ko

Request SRAM mem region ............

Hello,sram!

ß

这句即打印的
SRAM
内的字符串

/ # rmmod sram

Release SRAM mem region success!

SRAM is close

实验发现可以通过映射后的地址正常访问
SRAM


最后,这里举
SRAM
作为例子的还有一个原因是通过静态映射方式访问
SRAM
的话,我们可以预先知道
SRAM
映射后的内核虚拟地址(通过
IOADDRESS
计算)。这样的话就可以尝试在
SRAM
上做点文章。比如写个内存分配的
MODULE
管理
SRAM
或者其他方式,将一些
critical
的数据放在
SRAM
内运行,这样可以提高一些复杂程序的运行效率
(SRAM
速度比
SDRAM
快多了
)
,比如音视频的编解码过程中用到的较大的
buffer
等。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: