Linux核心訪問外設I/O資源的方式)
最後更新:2018-12-04
來源:互聯網
上載者:User
Linux核心訪問外設I/O資源的方式(轉)3/15/2009 1:27:34 PMhttp://www.diybl.com/course/6_system/linux/Linuxjs/200888/135069.htmlLinux核心訪問外設I/O資源的方式 Author: DongasDate: 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 sramRelease SRAM mem region success!SRAM is close 實驗發現可以通過映射後的地址正常訪問SRAM。 最後,這裡舉SRAM作為例子的還有一個原因是通過靜態映射方式訪問SRAM的話,我們可以預Crowdsourced Security Testing道SRAM映射後的核心虛擬位址(通過IOADDRESS計算)。這樣的話就可以嘗試在SRAM上做點文章。比如寫個記憶體配置的MODULE管理SRAM或者其他方式,將一些critical的資料放在SRAM內運行,這樣可以提高一些複雜程式的運行效率(SRAM速度比SDRAM快多了),比如音視頻的編解碼過程中用到的較大的buffer等。文章出處:http://www.diybl.com/course/6_system/linux/Linuxjs/200888/135069.html