轉載自:http://blog.csdn.net/aaronychen/article/details/4541553
我們知道在我們的驅動裏面一般操作的寄存器的位址都是虛擬位址, 然而一般在cpu的datasheet裏描述的寄存器的位址都是物理位址, 那linux內核是如何把我們驅動中指定操作的虛擬位址轉換成正真可定址並操作的物理位址的呢? 這篇文檔以s3c2410為例, 將詳細的描述這麼一個實現流程。
S3c2410使用的是arm920T的核,它支持MMU,正因為這樣它才可以進行虛擬位址到物理位址的轉換。而諸如使用arm7核的CPU一般都無法進行類似的轉換, 就在於它沒有MMU,所以它上面能跑的作業系統也是去掉了MMU功能後的linux如uClinux, 關於MMU的原理可參考相關文檔。
我們先來看檔Map.h
include/asm-arm/arch-s3c2410/Map.h:
#ifndef __ASSEMBLY__
#define S3C2410_ADDR(x) ((void __iomem __force *)0xF0000000 + (x))
#else
#define S3C2410_ADDR(x) (0xF0000000 + (x))
#endif
#define S3C2400_ADDR(x) S3C2410_ADDR(x)
/* interrupt controller is the first thing we put in, to make
* the assembly code for the irq detection easier
*/
#define S3C24XX_VA_IRQ S3C2410_ADDR(0x00000000)
#define S3C2400_PA_IRQ (0x14400000)
#define S3C2410_PA_IRQ (0x4A000000)
#define S3C24XX_SZ_IRQ SZ_1M
/* memory controller registers */
#define S3C24XX_VA_MEMCTRL S3C2410_ADDR(0x00100000)
#define S3C2400_PA_MEMCTRL (0x14000000)
#define S3C2410_PA_MEMCTRL (0x48000000)
#define S3C24XX_SZ_MEMCTRL SZ_1M
……
我們可以看到IRQ的寄存器虛擬位址定義為0xF0000000, 而物理位址為0x4A000000(這可從2410的datasheet上查到), memory控制器的寄存器虛擬位址定義為0xF0000000 + 0x00100000的位址處,物理位址為0x48000000, 其他如lcd 等寄存器都在這裏定義了虛擬位址,當然這裏僅僅是定義而已, 還沒有和物理位址達成映射的聯繫。 我們接著看。
我們來看2410的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
這裏定義了一個描述2410開發板的結構, 其中的map_io, init_irq, init_machine都會在系統跑起來的時候被調用, 我們這裏要看的是smdk2410_map_io, 這個函數完成後我們的虛擬位址和物理位址的映射關係就完成了。
arch/arm/mach-s3c2410/Mach-smdk2410.c:
static void __init smdk2410_map_io(void)
{
s3c24xx_init_io(smdk2410_iodesc, ARRAY_SIZE(smdk2410_iodesc)); //重點在這行
s3c24xx_init_clocks(0);
s3c24xx_init_uarts(smdk2410_uartcfgs, ARRAY_SIZE(smdk2410_uartcfgs));
}
該函數調用s3c24xx_init_io 來完成實質的東西。smdk2410_iodesc的定義如下:
arch/arm/mach-s3c2410/Mach-smdk2410.c:
static struct map_desc smdk2410_iodesc[] __initdata = {
/* nothing here yet */
};
這是一個保存虛擬位址和物理位址的映射關係表,內核通過這個參數的指導來完成映射關係。當然我們可以在這裏添加我們需要映射的東東了。不過這裏並沒有條目, 很簡單因為在後面還會有這樣的映射表。
arch/arm/plat-s3c24xx/cpu.c:
void __init s3c24xx_init_io(struct map_desc *mach_desc, int size)
{
unsigned long idcode = 0x0;
/* initialise the io descriptors we need for initialisation */
iotable_init(s3c_iodesc, ARRAY_SIZE(s3c_iodesc)); //完成映射
//獲取當前系統的CPU
if (cpu_architecture() >= CPU_ARCH_ARMv5) {
idcode = s3c24xx_read_idcode_v5();
} else {
idcode = s3c24xx_read_idcode_v4();
}
cpu = s3c_lookup_cpu(idcode);
if (cpu == NULL) {
printk(KERN_ERR "Unknown CPU type 0x%08lx/n", idcode);
panic("Unknown S3C24XX CPU");
}
printk("CPU %s (id 0x%08lx)/n", cpu->name, idcode);
if (cpu->map_io == NULL
cpu->init == NULL) {
printk(KERN_ERR "CPU %s support not enabled/n", cpu->name);
panic("Unsupported S3C24XX CPU");
}
(cpu->map_io)(mach_desc, size); //調用CPU相關的映射函數
}
完成映射關係的核心就在這個函數裏了, 對iotable_init()的調用完成了s3c_iodesc 裏的映射條目的映射。而這個函數也是最最核心的東東了。我們先來看要完成映射的條目有那些
arch/arm/plat-s3c24xx/cpu.c:
static struct map_desc s3c_iodesc[] __initdata = {
IODESC_ENT(GPIO), //GPIO寄存器虛實位址映射
IODESC_ENT(IRQ), //中斷寄存器虛實位址映射
IODESC_ENT(MEMCTRL), /…
IODESC_ENT(UART) //….
};
這個就是要進行虛實位址映射的映射表了, 裏面的每個條目都對應一個映射關係, 用map_desc來描述, 我們來看map_desc
include/asm-arm/mach/Map.h:
struct map_desc {
unsigned long virtual; //虛擬位址
unsigned long pfn; //對應的物理位址
unsigned long length; //映射長度
unsigned int type; //類型。
};
我們以IRQ的映射關係為例來看一下:
先看IODESC_ENT的定義:
#define IODESC_ENT(x) { (unsigned long)S3C24XX_VA_##x, __phys_to_pfn(S3C24XX_PA_##x), S3C24XX_SZ_##x, MT_DEVICE }
則 IODESC_ENT(IRQ) 就是:
{
(unsigned long)S3C24XX_VA_IRQ,
_phys_to_pfn(S3C24XX_PA_IRQ),
S3C24XX_SZ_IRQ,
MT_DEVICE
}
而S3C24XX_VA_IRQ 等都在include/asm-arm/arch-s3c2410/Map.h下定義過了。因此這個條目解釋就是虛擬位址為0xF0000000開始長度為1M的虛擬位址空間由MMU轉換成物理位址為0x4A000000開始的1M位址空間, 這正是IRQ寄存器物理位址所在位置。 條目定義好了, 剩下的就是要讓系統MMU知道並在遇到這樣一個虛擬位址時能正確映射到具體的物理位址。而這個工作就是由iotable_init完成的。
Arch/arm/mm/Mmu.c:
void __init iotable_init(struct map_desc *io_desc, int nr)
{
int i;
for (i = 0; i < nr; i++)
create_mapping(io_desc + i); //為每個條目創建一個映射表
}
我們接著看核心函數:create_mapping(), 它是如何讓MMU部件知道映射關係的
void __init create_mapping(struct map_desc *md)
{
unsigned long phys, addr, length, end;
const struct mem_type *type;
pgd_t *pgd;
//參數檢查, 是否可以進行映射
if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
printk(KERN_WARNING "BUG: not creating mapping for "
"0x%08llx at 0x%08lx in user region/n",
__pfn_to_phys((u64)md->pfn), md->virtual);
return;
}
if ((md->type == MT_DEVICE
md->type == MT_ROM) &&
md->virtual >= PAGE_OFFSET && md->virtual < VMALLOC_END) {
printk(KERN_WARNING "BUG: mapping for 0x%08llx at 0x%08lx "
"overlaps vmalloc space/n",
__pfn_to_phys((u64)md->pfn), md->virtual);
}
type = &mem_types[md->type]; //獲取memory類型
/*
* Catch 36-bit addresses
*/
if (md->pfn >= 0x100000) {
create_36bit_mapping(md, type);
return;
}
addr = md->virtual & PAGE_MASK; //得到虛擬位址
phys = (unsigned long)__pfn_to_phys(md->pfn); //得到物理位址
length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK)); //映射長度
if (type->prot_l1 == 0 && ((addr
phys
length) & ~SECTION_MASK)) {
printk(KERN_WARNING "BUG: map for 0x%08lx at 0x%08lx can not "
"be mapped using pages, ignoring./n",
__pfn_to_phys(md->pfn), addr);
return;
}
pgd = pgd_offset_k(addr);
end = addr + length;
do {
unsigned long next = pgd_addr_end(addr, end);
alloc_init_section(pgd, addr, next, phys, type);
phys += next - addr;
addr = next;
} while (pgd++, addr != end);
}
這個函數涉及到了相當多的MMU方面的知識, 一般系統中會有一個頁表,頁表中的每個條目都是一個映射關係,包括對應的物理位址,及該塊物理位址區域訪問的各種屬性等, 而該頁表的位址被保存在了MMU中的一個寄存器中。當我們在驅動中要訪問某個寄存器時, 一般都使用的是虛擬位址, 這個虛擬位址被分成了兩個區域, 高位址的若干位其實是個index, 用於指定需要頁表中的哪個條目來映射該虛擬位址, 而剩下的若干位則是偏移位址, 即具體要操作的物理位址處。
當MMU需要進行虛擬位址到物理位址的映射時會先通過該寄存器找到這張頁表,然後從虛擬位址的高若干位元來得到一個具體的映射條目, 從這個條目我們可以知道該虛擬位址所在區間對應的物理位址區間, 然後在通過虛擬位址的低若干位元來精確定位到具體的物理位址。
而這個函數實際上就是完成這個頁表下面的相關映射條目項的填寫工作, 以後當訪問到這個虛擬位址時, 就會使用這裏的條目進行虛實轉換, 從而最終找到我們需要的物理位址並進行讀寫操作。
沒有留言:
張貼留言