幾乎每一種外設都是通過讀寫設備上的寄存器來進行的,通常包括控制寄存器、狀態寄存器和數據寄存器三大類,外設的寄存器通常被連續地編址。根據CPU體系結構的不同,CPU對IO端口的編址方式有兩種: (1)I/O映射方式(I/O-mapped) 典型地,如X86處理器為外設專門實現了一個單獨的地址空間,稱為"I/O地址空間"或者"I/O端口空間",CPU通過專門的I/O指令(如X86的IN和OUT指令)來訪問這一空間中的地址單元。 (2)內存映射方式(Memory-mapped) RISC指令系統的CPU(如ARM、PowerPC等)通常只實現一個物理地址空間,外設I/O端口成為內存的一部分。此時,CPU可以象訪問一個內存單元那樣訪問外設I/O端口,而不需要設立專門的外設I/O指令。 但是,這兩者在硬件實現上的差異對于軟件來說是完全透明的,驅動程序開發人員可以將內存映射方式的I/O端口和外設內存統一看作是"I/O內存"資源。 一般來說,在系統運行時,外設的I/O內存資源的物理地址是已知的,由硬件的設計決定。但是CPU通常并沒有為這些已知的外設I/O內存資源的物理地址預定義虛擬地址范圍,驅動程序并不能直接通過物理地址訪問I/O內存資源,而必須將它們映射到核心虛地址空間內(通過頁表),然后才能根據映射所得到的核心虛地址范圍,通過訪內指令訪問這些I/O內存資源(也即是我們可以像讀寫RAM那樣直接讀寫I/O內存(外設寄存器)資源了)。 為了配置寄存器,我們需要知道寄存器在操作系統中的虛擬地址,因為驅動中要使用的是虛擬地址而非物理地址。在s3c2410中獲得GPIO各個寄存器的方法有兩種。 第一種是靜態映射法:使用系統初始化已經設置好的虛擬地址,這種方法在操作系統啟動過程中,頁表已經生成,可以直接使用,這時候可以使用內核導出函數在arch/arm/plat-s3c24xx/Gpio.c s3c2410_gpio_cfgpin s3c2410_gpio_getcfg s3c2410_gpio_setpin s3c2410_gpio_getpin等函數配置寄存器GPIO引腳功能 以setpin函數為例: void s3c2410_gpio_setpin(unsigned int pin, unsigned int to) { void __iomem *base = S3C24XX_GPIO_BASE(pin); /*算出端口所在組虛擬基址*/ unsigned long offs = S3C2410_GPIO_OFFSET(pin); /*算出端口所在組的偏移量(0-31)*/ unsigned long flags; unsigned long dat; local_irq_save(flags); dat = __raw_readl(base + 0x04); /*基址 + 0x04為DAT寄存器 */ dat &= ~(1 << offs); dat |= to << offs; __raw_writel(dat, base + 0x04); local_irq_restore(flags); } 分析代碼: void __iomem *base = S3C24XX_GPIO_BASE(pin); //定義一個指向所要配置的控制寄存器的基地址的指針,并賦初值(是個虛擬地址) /* #define S3C24XX_GPIO_BASE(x) S3C2410_GPIO_BASE(x) #define S3C2410_GPIO_BASE(pin) ((((pin)&~31)>>1)+ S3C24XX_VA_GPIO) #define S3C24XX_VA_GPIO ((S3C24XX_PA_GPIO-S3C24XX_PA_UART)+ S3C24XX_VA_UART) #define S3C24XX_VA_UART S3C_VA_UART #define S3C_VA_UART S3C_ADDR(0x01000000) #define S3C_ADDR(x)((void __iomem __force *)S3C_ADDR_BASE + (x)) #define S3C_ADDR_BASE (0xF4000000) */ __iomem是linux2.6.9內核中加入的特性。是用來個表示指針是指向一個I/O的內存空間。主要是為了驅動程序的通用性考慮。由于不同的CPU體系結構對I/O空間的表示可能不同。當使用__iomem時,編譯器會忽略對變量的檢查(因為用的是void __iomem)。若要對它進行檢查,當__iomem的指針和正常的指針混用時,就會發出一些警告。 dat = __raw_readl(base + 0x04);//取原寄存器的值初始化dat dat &= ~(1 << offs); //掩碼運算,清零對應位 dat |= to << offs; //功能碼生成,配置對應位的值 __raw_writel(dat, base + 0x04); //寫入控制寄存器 __raw_readl和__raw_writel: Linux對I/O的操作都定義在asm/io.h中,相應的在arm平臺下,就在asm-arm/io.h中。 #define __raw_writel(v,a)(__chk_io_ptr(a), *(volatile unsigned int __force *)(a) = (v)) #define __raw_readl(a)(__chk_io_ptr(a), *(volatile unsigned int __force *)(a)) 在include\linux\compiler.h中: #ifdef __CHECKER__ …… extern void __chk_io_ptr(void __iomem *); #else …… # define __chk_io_ptr(x) (void)0 …… #endif __raw_readl(a)展開是:((void)0, *(volatile unsigned int _force *)(a))。在定義了__CHECKER__的時候先調用__chk_io_ptr檢查該地址,否則__chk_io_ptr什么也不做,* (volatile unsigned int _force *)(a)就是返回地址為a處的值。(void)xx的做法有時候是有用的,例如編譯器打開了檢查未使用的參數的時候需要將沒有用到的參數這么弄一下才能 編譯通過。CPU對I/O的物理地址的編程方式有兩種:一種是I/O映射,一種是內存映射。 __raw_readl和__raw_writel等是原始的操作I/O的方法,由此派生出來的操作方法有:inb、outb、 _memcpy_fromio、readb、writeb、ioread8、iowrite8等。 靜態映射方式下虛擬地址的建立是由操作系統完成的,不需要用戶進行干涉。在arch/arm/mach-s3c2440/mach-mini2440.c中,其中包含了io端口虛擬地址到物理地址映射的初始化函數: MACHINE_START(S3C2440, "s3cARM s3c2440 development board") 。。。。。 .map_io = s3c2440_map_io, 。。。。。 MACHINE_END static void __init s3c2440_map_io(void) { /*該函數完成靜態的物理地址到虛擬地址的轉換*/ s3c24xx_init_io(s3c2440_iodesc, ARRAY_SIZE(s3c2440_iodesc)); s3c24xx_init_clocks(12000000); s3c24xx_init_uarts(s3c2440_uartcfgs, ARRAY_SIZE(s3c2440_uartcfgs)); } 第二種是動態映射法,通過ioremap函數獲得相應的虛擬地址。Linux在io.h頭文件中聲明了函數ioremap(),用來將I/O內存資源的物理地址映射到核心虛地址空間(3GB-4GB)中,原型如下: void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags); iounmap函數用于取消ioremap()所做的映射,原型如下: void iounmap(void * addr); 這兩個函數都是實現在arch/XXX/mm/ioremap.c文件中。 我們要特別強調驅動程序中mmap函數的實現方法。用mmap映射一個設備,意味著使用戶空間的一段地址關聯到設備內存上,這使得只要程序在分配的地址范圍內進行讀取或者寫入,實際上就是對設備的訪問。 在Linux源代碼中進行包含"ioremap"文本的搜索,發現真正出現的ioremap的地方相當少。所以筆者追根索源地尋找I/O操作的物理地址轉換到虛擬地址的真實所在,發現Linux有替代ioremap的語句,但是這個轉換過程卻是不可或缺的。有沒有出現ioremap是次要的,關鍵問題是有無虛擬地址和物理地址的轉換! 下面的程序在啟動的時候保留一段內存,然后使用ioremap將它映射到內核虛擬空間,同時又用remap_page_range映射到用戶虛擬空間,這樣一來,內核和用戶都能訪問。如果在內核虛擬地址將這段內存初始化串"abcd",那么在用戶虛擬地址能夠讀出來: /************mmap_ioremap.c**************/ #include #include #include #include #include #include #include MODULE_PARM(mem_start, "i"); MODULE_PARM(mem_size, "i"); static int mem_start = 101, mem_size = 10; static char *reserve_virt_addr; static int major; int mmapdrv_open(struct inode *inode, struct file *file); int mmapdrv_release(struct inode *inode, struct file *file); int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma); static struct file_operations mmapdrv_fops = { owner: THIS_MODULE, mmap: mmapdrv_mmap, open: mmapdrv_open, release:mmapdrv_release, }; int init_module(void) { if ((major = register_chrdev(0, "mmapdrv", &mmapdrv_fops)) < 0) { printk("mmapdrv: unable to register character device\n"); return ( - EIO); } printk("mmap device major = %d\n", major); printk("high memory physical address 0x%ldM\n", virt_to_phys(high_memory) / 1024 / 1024); reserve_virt_addr = ioremap(mem_start *1024 * 1024, mem_size *1024 * 1024); printk("reserve_virt_addr = 0x%lx\n", (unsigned long)reserve_virt_addr); if (reserve_virt_addr) { int i; for (i = 0; i < mem_size *1024 * 1024; i += 4) { reserve_virt_addr = 'a'; reserve_virt_addr[i + 1] = 'b'; reserve_virt_addr[i + 2] = 'c'; reserve_virt_addr[i + 3] = 'd'; } } else { unregister_chrdev(major, "mmapdrv"); return - ENODEV; } return 0; } /* remove the module */ void cleanup_module(void) { if (reserve_virt_addr)iounmap(reserve_virt_addr); unregister_chrdev(major, "mmapdrv"); return ; } int mmapdrv_open(struct inode *inode, struct file *file) { MOD_INC_USE_COUNT; return (0); } int mmapdrv_release(struct inode *inode, struct file *file) { MOD_DEC_USE_COUNT; return (0); } int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma) { unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; unsigned long size = vma->vm_end - vma->vm_start; if (size > mem_size *1024 * 1024) { printk("size too big\n"); return ( - ENXIO); } offset = offset + mem_start * 1024 * 1024; /* we do not want to have this area swapped out, lock it */ vma->vm_flags |= VM_LOCKED; if (remap_page_range(vma, vma->vm_start, offset, size, PAGE_SHARED)) { printk("remap page range failed\n"); return - ENXIO; } return (0); } remap_page_range函數的功能是構造用于映射一段物理地址的新頁表,實現了內核空間與用戶空間的映射,其原型如下: int remap_page_range(vma_area_struct *vma, unsigned long from, unsigned long to, unsigned long size, pgprot_tprot); 想參加嵌入式學習的朋友可聯系郭老師QQ754634522 使用mmap最典型的例子是顯示卡的驅動,將顯存空間直接從內核映射到用戶空間將可提供顯存的讀寫效率。(在內核驅動程序的初始化階段,通過ioremap()將物理地址映射到內核虛擬空間;在驅動程序的mmap系統調用中,使用remap_page_range()將該塊ROM映射到用戶虛擬空間。這樣內核空間和用戶空間都能訪問這段被映射后的虛擬地址。) |