`

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

阅读更多

我们知道默认外设 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 等。

分享到:
评论

相关推荐

    Linux内核访问外设I O资源的方式(转)

    《深入理解Linux内核访问外设I/O资源的方式》 在深入探讨Linux内核如何访问外部设备的输入/输出(I/O)资源之前,我们有必要先了解I/O端口与I/O内存的基本概念及其区别。这不仅对于理解操作系统内核与硬件交互的...

    Linux内核访问外设IO资源的方式.pdf

    ### Linux内核访问外设IO资源的方式 #### 一、概述 在Linux内核中,为了能够有效地控制和管理各种外部设备(例如SRAM、硬件接口寄存器等),需要将这些外设的I/O资源映射到内核空间中。通常来说,Linux内核支持两...

    I/O管理程序(最新发表) I/O管理程序(最新发表)

    总的来说,Linux内核的I/O管理程序是一个复杂而精细的设计,它在硬件与软件之间搭建了一座桥梁,使得操作系统能够有效地利用和管理各种I/O资源。无论是系统开发者还是设备驱动程序员,都需要对这一机制有深入的理解...

    Linux内核分析与应用课件第9章(二)IO空间的管理.pdf

    本章节主要讲解了 Linux 内核在 I/O 空间管理方面的知识点,包括 I/O 控制器、I/O 空间的管理、I/O 端口和 I/O 内存的访问方式、I/O 资源管理等。 I/O 控制器是计算机中的一个实体,主要职责是控制一个或多个 I/O ...

    嵌入式Linux内核及其驱动开发

    通过USB驱动程序的开发示例,文章展示了如何向Linux内核提供设备文件I/O接口,以及如何通过标准的核心服务如内存分配、中断转发和等待队列来实现USB设备的驱动开发。类似地,通过GPIO驱动程序的开发示例,文章讲述了...

    Linux的内存和IO访问技术

    在x86处理器中,内存空间与I/O空间是两种不同的资源访问方式。I/O空间通过特定的指令“in”和“out”来访问端口号,这个端口号是外设寄存器地址的表示。然而,并非所有处理器都支持I/O空间,例如ARM和PowerPC等...

    Linux内核源代码情景分析 (上下册 高清非扫描 )

    - Linux内核通过复杂的算法来有效地管理内存资源,以满足不同进程的需求。 - **2.2 地址映射的全过程** - 地址映射是指将进程的虚拟地址空间映射到物理内存的过程。 - 映射过程中涉及到页表、段表等数据结构的...

    LINUX编程白皮书 (全集)

    第一部分 Linux内核 前言 第1章 硬件基础与软件基础 6 1.1 硬件基础 6 1.1.1 CPU 7 1.1.2 存储器 8 1.1.3 总线 8 1.1.4 控制器和外设 8 1.1.5 地址空间 9 1.1.6 时钟 9 1.2 软件基础 9 1.2.1 计算机语言 9 1.2.2 ...

    Hi3511/Hi3512 Linux 内核与标准内核差异说明

    这部分代码负责初始化I/O映射表,确保内核能够通过内存访问方式操作外设。 **3. 平台相关的修改文件** 为了适应Hi3511/Hi3512平台的特点,可能需要修改的标准内核文件包括但不限于: - `arch/arm/mach-hi35xx/`: ...

    Linux内核配置参考

    - **在统计信息中包含进程等候系统资源所花费的时间**:帮助监控和分析进程在等待CPU、I/O操作、内存交换等方面消耗的时间。 - **UTS名字空间支持**:允许进程拥有独立的主机名和域名,适用于容器化环境中的隔离需求...

    深入理解LINUX内核

    由于提供的文件信息中含有大量的重复内容和非正文信息,我将直接从“深入理解LINUX内核”这一标题和“深入理解LINUX内核,对内核中使用的最重要的数据结构、算法和程序设计诀窍进行一次遍历。希望理解内核原理的朋友...

    国嵌嵌入式Linux内核驱动深入教班材配套2440(最新)

    5. **I/O端口和总线**:解释I/O端口的使用和总线通信,如I2C、SPI和UART等常见外设总线。 6. **设备树**:详细介绍设备树在嵌入式Linux中的应用,它是如何描述硬件配置,帮助内核初始化硬件资源的。 7. **S3C2440...

    Linux IO 之 IO与网络模型.pdf

    对于IO行为,特别是CPU和IO之间的交互,Linux内核通过DMA(直接内存访问)实现,它允许外设直接与内存交换数据,减轻了CPU的负担,使得CPU与IO可以并行工作,从而提高了系统的整体性能。 针对特定的应用场景,如...

    ioremap和mmap作为linux内存的关键操作.docx

    ioremap 和 mmap 作为 Linux 内存的关键操作 Linux 操作系统中,对外设的...ioremap 和 mmap 是 Linux 内存的关键操作,用于将外设 I/O 内存资源映射到内核或进程的虚拟地址空间上,使得驱动程序可以访问这些资源。

    Linux-2.4.20内核源代码

    通过研究这个版本的内核,开发者不仅可以了解Linux内核的基本架构,还可以学习到如何阅读和分析复杂的开源项目,这对于成为一名出色的Linux开发者至关重要。同时,这也为硬件驱动的开发、系统调优和安全研究提供了...

    基于CK810 LINUX3.0内核的移植实现.pdf

    平台抽象层则关注具体的硬件平台,如启动流程、芯片选择、定时器、I/O接口和中断控制等。 系统调用是连接内核和用户程序的关键,它是操作系统向用户程序提供的服务接口。在Linux中,系统调用通过软中断机制实现,如...

Global site tag (gtag.js) - Google Analytics