嵌入式Linux之我行——內核訪問外設I/O資源的方式

嵌入式Linux之我行,主要講述和總結了本人在學習嵌入式linux中的每個步驟。一爲總結經驗,二希望能給想入門嵌入式Linux的朋友提供方便。如有錯誤之處,謝請指正。

我們知道默認外設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等。


發佈了43 篇原創文章 · 獲贊 11 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章